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) }