-
Florent Aide authoredFlorent Aide authored
config.go 10.37 KiB
package runner
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/rs/zerolog"
"github.com/spf13/viper"
)
// Variable ...
type Variable struct {
Name string `mapstructure:"name"`
Value string `mapstructure:"value"`
}
type Chart struct {
Type string `mapstructure:"type"`
Path string `mapstructure:"path"`
}
type Arg struct {
Flag string `mapstructure:"flag"`
Value string `mapstructure:"value"`
}
type Create struct {
Type string `mapstructure:"type"`
Name string `mapstructure:"name"`
Args []Arg `mapstructure:"args"`
}
func (k CmdCreate) BuildArgs(namespace string, args []Arg) []string {
output := []string{
"-n", namespace,
"create",
k.Type,
k.Name,
"--dry-run=client",
"-o", "yaml"}
for _, arg := range args {
output = append(output, arg.Flag, arg.Value)
}
return output
}
// Spec ...
type Spec struct {
Inherit string `mapstructure:"inherit"`
NameSpace string `mapstructure:"namespace"`
Variables []Variable `mapstructure:"variables"`
Charts map[string]Chart `mapstructure:"charts"`
Creates []Create `mapstructure:"create"`
}
// Config is the configuration we get after parsing our beaver.yml file
type Config struct {
APIVersion string `mapstructure:"apiVersion"`
Kind string `mapstructure:"kind"`
Spec Spec `mapstructure:"spec"`
}
// NewConfig returns a *Config
func NewConfig(configDir string) (*Config, error) {
v := viper.New()
v.SetConfigName("beaver")
v.AddConfigPath(configDir)
if err := v.ReadInConfig(); err != nil {
return nil, err
}
var config Config
cfg := &config
if err := v.Unmarshal(&cfg); err != nil {
return nil, err
}
return cfg, nil
}
func NewCmdConfig(logger zerolog.Logger, rootDir, configDir string, dryRun bool) *CmdConfig {
cmdConfig := &CmdConfig{}
cmdConfig.DryRun = dryRun
cmdConfig.RootDir = rootDir
cmdConfig.Layers = append(cmdConfig.Layers, configDir)
cmdConfig.Spec.Charts = make(map[string]CmdChart)
cmdConfig.Spec.Creates = make(map[CmdCreate][]Arg)
cmdConfig.Namespace = ""
cmdConfig.Logger = logger
return cmdConfig
}
func (c *CmdConfig) Initialize(tmpDir string) error {
if len(c.Layers) != 1 {
return fmt.Errorf("you must only have one layer when calling Initialize, found: %d", len(c.Layers))
}
var (
weNeedToGoDeeper = true
configLayers []*Config
)
resolvedConfigDir := filepath.Join(c.RootDir, c.Layers[0])
absConfigDir, err := filepath.Abs(resolvedConfigDir)
if err != nil {
return fmt.Errorf("failed to find abs() for %s: %w", resolvedConfigDir, err)
}
dir := absConfigDir
for weNeedToGoDeeper {
config, err := c.newConfigFromDir(dir)
if err != nil {
return fmt.Errorf("failed to create config from %s: %w", dir, err)
}
// first config dir must return a real config...
// others can be skipped
if config == nil && len(configLayers) == 0 {
return fmt.Errorf("failed to find config in dir: %s", dir)
}
absDir, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to find abs() for %s: %w", dir, err)
}
c.Layers = append(c.Layers, absDir)
configLayers = append(configLayers, config)
if config == nil || config.Spec.Inherit == "" {
weNeedToGoDeeper = false
} else {
resolvedDir := filepath.Join(absDir, config.Spec.Inherit)
newDir, err := filepath.Abs(resolvedDir)
if err != nil {
return fmt.Errorf("failed to find abs() for %s: %w", resolvedDir, err)
}
dir = newDir
}
}
// reverse our layers list
for i, j := 0, len(configLayers)-1; i < j; i, j = i+1, j-1 {
configLayers[i], configLayers[j] = configLayers[j], configLayers[i]
}
fmt.Printf("%+v\n", configLayers)
for _, config := range configLayers {
fmt.Printf("%+v\n", config)
c.Namespace = config.Spec.NameSpace
c.MergeVariables(config)
for k, chart := range config.Spec.Charts {
c.Spec.Charts[k] = cmdChartFromChart(chart)
}
for _, k := range config.Spec.Creates {
cmdCreate := CmdCreate{Type: k.Type, Name: k.Name}
c.Spec.Creates[cmdCreate] = k.Args
}
}
for i, j := 0, len(c.Layers)-1; i < j; i, j = i+1, j-1 {
c.Layers[i], c.Layers[j] = c.Layers[j], c.Layers[i]
}
c.populate()
if err := c.hydrate(tmpDir); err != nil {
return fmt.Errorf("failed to hydrate tmpDir (%s): %w", tmpDir, err)
}
return nil
}
func (c *CmdConfig) newConfigFromDir(dir string) (*Config, error) {
cfg, err := NewConfig(dir)
var cfgNotFound bool
if err != nil {
_, cfgNotFound = err.(viper.ConfigFileNotFoundError)
if !cfgNotFound {
return nil, err
}
}
return cfg, nil
}
type CmdCreate struct {
Type string `mapstructure:"type"`
Name string `mapstructure:"name"`
}
type CmdConfig struct {
Spec CmdSpec
RootDir string
Layers []string
Namespace string
Logger zerolog.Logger
DryRun bool
}
type CmdSpec struct {
Variables []Variable
Charts CmdCharts
Ytt Ytt
Creates map[CmdCreate][]Arg
}
type Ytt []string
func (y Ytt) BuildArgs(layers, compiled []string) []string {
// ytt -f $chartsTmpFile --file-mark "$(basename $chartsTmpFile):type=yaml-plain"\
// -f base/ytt/ -f base/ytt.yml -f ns1/ytt/ -f ns1/ytt.yml
var args []string
for _, c := range compiled {
args = append(args, "-f", c, fmt.Sprintf("--file-mark=%s:type=yaml-plain", filepath.Base(c)))
}
for _, layer := range layers {
for _, ext := range []string{"", ".yaml", ".yml"} {
entry := fmt.Sprintf("ytt%s", ext)
entryPath := filepath.Join(layer, entry)
if _, err := os.Stat(entryPath); !os.IsNotExist(err) {
args = append(args, "-f", entryPath)
}
}
}
return args
}
type CmdCharts map[string]CmdChart
type CmdChart struct {
Type string
Path string
ValuesFileNames []string
}
const (
HelmType = "helm"
YttType = "ytt"
)
// BuildArgs is in charge of producing the argument list to be provided
// to the cmd
func (c CmdChart) BuildArgs(name, namespace string) ([]string, error) {
var args []string
switch c.Type {
case HelmType:
// helm template name vendor/helm/mychart/ --namespace ns1 -f base.values.yaml -f ns.yaml -f ns.values.yaml
args = append(args, "template", name, c.Path, "--namespace", namespace)
case YttType:
args = append(args, "-f", c.Path)
default:
return nil, fmt.Errorf("unsupported chart %s type: %q", c.Path, c.Type)
}
for _, vFile := range c.ValuesFileNames {
args = append(args, "-f", vFile)
}
return args, nil
}
func cmdChartFromChart(c Chart) CmdChart {
return CmdChart{
Type: c.Type,
Path: c.Path,
ValuesFileNames: nil,
}
}
// hydrate expands templated variables in our config with concrete values
func (c *CmdConfig) hydrate(tmpDir string) error {
c.Logger.Debug().Str("charts", fmt.Sprintf("%+v\n", c.Spec.Charts)).Msg("before hydrate")
if err := c.hydrateFiles(tmpDir); err != nil {
return err
}
c.Logger.Debug().Str("charts", fmt.Sprintf("%+v\n", c.Spec.Charts)).Msg("after hydrate")
return nil
}
func (c *CmdConfig) prepareVariables(v []Variable) map[string]string {
variables := make(map[string]string)
for _, variable := range v {
variables[variable.Name] = variable.Value
}
variables["namespace"] = c.Namespace
return variables
}
func (c *CmdConfig) populate() {
c.Spec.Charts = FindFiles(c.Layers, c.Spec.Charts)
c.Spec.Ytt = findYttFiles(c.Layers)
}
func findYttFiles(layers []string) []string {
var result []string
for _, layer := range layers {
fPath := filepath.Join(layer, "ytt")
baseYttDirInfo, err := os.Stat(fPath)
if err == nil && baseYttDirInfo.IsDir() {
result = append(result, fPath)
}
for _, ext := range []string{"yaml", "yml"} {
fPath := filepath.Join(layer, fmt.Sprintf("ytt.%s", ext))
baseYttFileInfo, err := os.Stat(fPath)
if err == nil && !baseYttFileInfo.IsDir() {
result = append(result, fPath)
}
}
}
return result
}
func FindFiles(layers []string, charts map[string]CmdChart) map[string]CmdChart {
for name, chart := range charts {
files := findYaml(layers, name)
chart.ValuesFileNames = append(chart.ValuesFileNames, files...)
charts[name] = chart
}
return charts
}
func findYaml(layers []string, name string) []string {
var files []string
for _, layer := range layers {
for _, ext := range []string{"yaml", "yml"} {
fpath := filepath.Join(layer, fmt.Sprintf("%s.%s", name, ext))
if _, err := os.Stat(fpath); err == nil {
files = append(files, fpath)
}
}
}
return files
}
func hydrateFiles(tmpDir string, variables map[string]string, paths []string) ([]string, error) {
var result []string
for _, path := range paths {
fileInfo, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("hydrateFiles could not stat file or dir %s: %w", path, err)
}
if fileInfo.IsDir() {
result = append(result, path)
continue
}
if tmpl, err := template.New(filepath.Base(path)).ParseFiles(path); err != nil {
return nil, err
} else {
ext := filepath.Ext(path)
if tmpFile, err := ioutil.TempFile(tmpDir, fmt.Sprintf("%s-*%s", strings.TrimSuffix(filepath.Base(path), ext), ext)); err != nil {
return nil, fmt.Errorf("hydrateFiles failed to create tempfile: %w", err)
} else {
defer func() {
_ = tmpFile.Close()
}()
if err := tmpl.Execute(tmpFile, variables); err != nil {
return nil, fmt.Errorf("hydrateFiles failed to execute template: %w", err)
}
result = append(result, tmpFile.Name())
}
}
}
return result, nil
}
func (c *CmdConfig) hydrateFiles(dirName string) error {
variables := c.prepareVariables(c.Spec.Variables)
for key, chart := range c.Spec.Charts {
if paths, err := hydrateFiles(dirName, variables, chart.ValuesFileNames); err != nil {
return err
} else {
chart.ValuesFileNames = paths
c.Spec.Charts[key] = chart
}
}
return nil
}
// MergeVariables takes a config (from a file, not a cmd one) and import its
// variables into the current cmdconfig by replacing old ones
// and adding the new ones
func (c *CmdConfig) MergeVariables(other *Config) {
for _, variable := range other.Spec.Variables {
c.overlayVariable(variable)
}
}
// overlayVariable takes a variable in and either replaces an existing variable
// of the same name or create a new variable in the config if no matching name
// is found
func (c *CmdConfig) overlayVariable(v Variable) {
// find same variable by name and replace is value
// if not found then create the variable
for index, originalVariable := range c.Spec.Variables {
if originalVariable.Name == v.Name {
c.Spec.Variables[index].Value = v.Value
return
}
}
c.Spec.Variables = append(c.Spec.Variables, v)
}