// Copyright (C) 2016-2017 Vincent Ambo // // 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"` // 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.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 (explicit > import > include > global > default) 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(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 } 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. func (ctx *Context) mergeContextValues() []ResourceSet { updated := make([]ResourceSet, len(ctx.ResourceSets)) for i, rs := range ctx.ResourceSets { merged := loadDefaultValues(&rs, ctx) merged = util.Merge(merged, &ctx.Global) merged = util.Merge(merged, &ctx.ImportedVars) merged = util.Merge(merged, &ctx.ExplicitVars) rs.Values = *merged updated[i] = rs } return updated } // Loads and merges default values for a resource set collection from path/to/set/default.{json|yaml}. // YAML takes precedence over JSON. // Default values in resource set collections have the lowest priority possible. func loadDefaultValues(rs *ResourceSet, c *Context) *map[string]interface{} { var defaultVars map[string]interface{} for _, filename := range util.DefaultFilenames { err := util.LoadData(path.Join(c.BaseDir, rs.Path, filename), &defaultVars) if err == nil { return util.Merge(&defaultVars, &rs.Values) } } // 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.Split(v, "=") 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 }