diff options
Diffstat (limited to 'ops/kontemplate/context/context.go')
-rw-r--r-- | ops/kontemplate/context/context.go | 266 |
1 files changed, 266 insertions, 0 deletions
diff --git a/ops/kontemplate/context/context.go b/ops/kontemplate/context/context.go new file mode 100644 index 000000000000..2d0378a0ec23 --- /dev/null +++ b/ops/kontemplate/context/context.go @@ -0,0 +1,266 @@ +// Copyright (C) 2016-2019 Vincent Ambo <mail@tazj.in> +// +// This file is part of Kontemplate. +// +// Kontemplate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +package context + +import ( + "fmt" + "path" + "strings" + + "github.com/tazjin/kontemplate/util" +) + +type ResourceSet struct { + // Name of the resource set. This can be used in include/exclude statements during kontemplate runs. + Name string `json:"name"` + + // Path to the folder containing the files for this resource set. This defaults to the value of the 'name' field + // if unset. + Path string `json:"path"` + + // Values to include when interpolating resources from this resource set. + Values map[string]interface{} `json:"values"` + + // Args to pass on to kubectl for this resource set. + Args []string `json:"args"` + + // Nested resource sets to include + Include []ResourceSet `json:"include"` + + // Parent resource set for flattened resource sets. Should not be manually specified. + Parent string +} + +type Context struct { + // The name of the kubectl context + Name string `json:"context"` + + // Global variables that should be accessible by all resource sets + Global map[string]interface{} `json:"global"` + + // File names of YAML or JSON files including extra variables that should be globally accessible + VariableImportFiles []string `json:"import"` + + // The resource sets to include in this context + ResourceSets []ResourceSet `json:"include"` + + // Variables imported from additional files + ImportedVars map[string]interface{} + + // Explicitly set variables (via `--var`) that should override all others + ExplicitVars map[string]interface{} + + // This field represents the absolute path to the context base directory and should not be manually specified. + BaseDir string +} + +func contextLoadingError(filename string, cause error) error { + return fmt.Errorf("Context loading failed on file %s due to: \n%v", filename, cause) +} + +// Attempt to load and deserialise a Context from the specified file. +func LoadContext(filename string, explicitVars *[]string) (*Context, error) { + var ctx Context + err := util.LoadData(filename, &ctx) + + if err != nil { + return nil, contextLoadingError(filename, err) + } + + ctx.BaseDir = path.Dir(filename) + + // Prepare the resource sets by resolving parents etc. + ctx.ResourceSets = flattenPrepareResourceSetPaths(&ctx.BaseDir, &ctx.ResourceSets) + + // Add variables explicitly specified on the command line + ctx.ExplicitVars, err = loadExplicitVars(explicitVars) + if err != nil { + return nil, fmt.Errorf("Error setting explicit variables: %v\n", err) + } + + // Add variables loaded from import files + ctx.ImportedVars, err = ctx.loadImportedVariables() + if err != nil { + return nil, contextLoadingError(filename, err) + } + + // Merge variables defined at different levels. The + // `mergeContextValues` function is documented with the merge + // hierarchy. + ctx.ResourceSets = ctx.mergeContextValues() + + if err != nil { + return nil, contextLoadingError(filename, err) + } + + return &ctx, nil +} + +// Kontemplate supports specifying additional variable files with the +// `import` keyword. This function loads those variable files and +// merges them together with the context's other global variables. +func (ctx *Context) loadImportedVariables() (map[string]interface{}, error) { + allImportedVars := make(map[string]interface{}) + + for _, file := range ctx.VariableImportFiles { + // Ensure that the filename is not merged with the baseDir if + // it is set to an absolute path. + var filePath string + if path.IsAbs(file) { + filePath = file + } else { + filePath = path.Join(ctx.BaseDir, file) + } + + var importedVars map[string]interface{} + err := util.LoadData(filePath, &importedVars) + + if err != nil { + return nil, err + } + + allImportedVars = *util.Merge(&allImportedVars, &importedVars) + } + + return allImportedVars, nil +} + +// Correctly prepares the file paths for resource sets by inferring implicit paths and flattening resource set +// collections, i.e. resource sets that themselves have an additional 'include' field set. +// Those will be regarded as a short-hand for including multiple resource sets from a subfolder. +// See https://github.com/tazjin/kontemplate/issues/9 for more information. +func flattenPrepareResourceSetPaths(baseDir *string, rs *[]ResourceSet) []ResourceSet { + flattened := make([]ResourceSet, 0) + + for _, r := range *rs { + // If a path is not explicitly specified it should default to the resource set name. + // This is also the classic behaviour prior to kontemplate 1.2 + if r.Path == "" { + r.Path = r.Name + } + + // Paths are made absolute by resolving them relative to the context base, + // unless absolute paths were specified. + if !path.IsAbs(r.Path) { + r.Path = path.Join(*baseDir, r.Path) + } + + if len(r.Include) == 0 { + flattened = append(flattened, r) + } else { + for _, subResourceSet := range r.Include { + if subResourceSet.Path == "" { + subResourceSet.Path = subResourceSet.Name + } + + subResourceSet.Parent = r.Name + subResourceSet.Name = path.Join(r.Name, subResourceSet.Name) + subResourceSet.Path = path.Join(r.Path, subResourceSet.Path) + subResourceSet.Values = *util.Merge(&r.Values, &subResourceSet.Values) + flattened = append(flattened, subResourceSet) + } + } + } + + return flattened +} + +// Merges the context and resource set variables according in the +// desired precedence order. +// +// For now the reasoning behind the merge order is from least specific +// in relation to the cluster configuration, which means that the +// precedence is (in ascending order): +// +// 1. Default values in resource sets. +// 2. Values imported from files (via `import:`) +// 3. Global values in a cluster configuration +// 4. Values set in a resource set's `include`-section +// 5. Explicit values set on the CLI (`--var`) +// +// For a discussion on the reasoning behind this order, please consult +// https://github.com/tazjin/kontemplate/issues/142 +func (ctx *Context) mergeContextValues() []ResourceSet { + updated := make([]ResourceSet, len(ctx.ResourceSets)) + + // Merging has to happen separately for every individual + // resource set to make use of the default values: + for i, rs := range ctx.ResourceSets { + // Begin by loading default values from the resource + // sets configuration. + // + // Resource sets are used across different cluster + // contexts and the default values in them have the + // lowest precedence. + defaultValues := loadDefaultValues(&rs, ctx) + + // Continue by merging default values with values + // imported from external files. Those values are also + // used across cluster contexts, but have higher + // precedence than defaults. + merged := util.Merge(defaultValues, &ctx.ImportedVars) + + // Merge global values defined in the cluster context: + merged = util.Merge(merged, &ctx.Global) + + // Merge values configured in the resource set's + // `include` section: + merged = util.Merge(merged, &rs.Values) + + // Merge values defined explicitly on the CLI: + merged = util.Merge(merged, &ctx.ExplicitVars) + + // Continue with the newly merged resource set: + rs.Values = *merged + updated[i] = rs + } + + return updated +} + +// Loads default values for a resource set collection from +// path/to/set/default.{json|yaml}. +func loadDefaultValues(rs *ResourceSet, c *Context) *map[string]interface{} { + var defaultVars map[string]interface{} + + for _, filename := range util.DefaultFilenames { + err := util.LoadData(path.Join(rs.Path, filename), &defaultVars) + if err == nil { + return &defaultVars + } + } + + // The actual error is not inspected here. The reasoning for + // this is that in case of serious problems (e.g. permission + // issues with the folder / folder not existing) failure will + // occur a bit later anyways. + // + // Otherwise we'd have to differentiate between + // file-not-found-errors (no default values specified) and + // other errors here. + return &rs.Values +} + +// Prepares the variables specified explicitly via `--var` when +// executing kontemplate for adding to the context. +func loadExplicitVars(vars *[]string) (map[string]interface{}, error) { + explicitVars := make(map[string]interface{}, len(*vars)) + + for _, v := range *vars { + varParts := strings.SplitN(v, "=", 2) + if len(varParts) != 2 { + return nil, fmt.Errorf(`invalid explicit variable provided (%s), name and value should be separated with "="`, v) + } + + explicitVars[varParts[0]] = varParts[1] + } + + return explicitVars, nil +} |