Skip to content
Snippets Groups Projects
cmd.go 5.85 KiB
package runner

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"text/template"

	"github.com/go-cmd/cmd"
	"github.com/rs/zerolog"
	"gopkg.in/yaml.v2"
)

func RunCMD(name string, args ...string) (err error, stdout, stderr []string) {
	// helm template -f base.yaml -f base.values.yaml -f ns.yaml -f ns.values.yaml
	// ytt -f /chart-folder -f base.yaml -f ns.yaml -v ... -v ...
	c := cmd.NewCmd(name, args...)
	statusChan := c.Start()
	status := <-statusChan
	if status.Error != nil {
		return err, status.Stdout, status.Stderr
	}
	stdout = status.Stdout
	stderr = status.Stderr
	return
}

func NewCmdConfig(logger zerolog.Logger, configDir string, namespace string) (*CmdConfig, error) {
	cmdConfig := &CmdConfig{}
	cmdConfig.Spec.Charts.Helm = make(map[string]CmdHelmChart)
	cmdConfig.Spec.Charts.Ytt = make(map[string]CmdYttChart)
	cmdConfig.Namespace = namespace
	cmdConfig.Logger = logger

	baseCfg, err := NewConfig(configDir)
	if err != nil {
		return nil, err
	}

	nsCfgDir := filepath.Join(configDir, namespace)
	nsCfg, err := NewConfig(nsCfgDir)
	if err != nil && err != os.ErrNotExist {
		return nil, err
	}

	// first "import" all variables from baseCfg
	cmdConfig.Spec.Variables = baseCfg.Spec.Variables
	// then merge in all variables from the nsCfg
	cmdConfig.MergeVariables(nsCfg)

	// TODO:
	// - merge baseCfg & nsCfg charts
	if err := cmdConfig.importCharts(baseCfg); err != nil {
		return nil, err
	}
	if err := cmdConfig.importCharts(nsCfg); err != nil {
		return nil, err
	}

	// - hydrate
	if err := cmdConfig.hydrate(); err != nil {
		return nil, err
	}

	return cmdConfig, nil
}

type CmdConfig struct {
	Spec      CmdSpec
	Namespace string
	Logger    zerolog.Logger
}

type CmdSpec struct {
	Variables []Variable
	Charts    CmdChart
}

type CmdChart struct {
	Helm map[string]CmdHelmChart
	Ytt  map[string]CmdYttChart
}

type CmdHelmChart struct {
	Type   string
	Name   string
	Values []string
}
type CmdYttChart struct {
	Type   string
	Name   string
	Files  []string
	Values []Value
}

// hydrate expands templated variables in our config with concrete values
func (c *CmdConfig) hydrate() error {
	if err := c.hydrateHelmCharts(); err != nil {
		return err
	}
	if err := c.hydrateYttCharts(); err != nil {
		return err
	}
	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) hydrateYttCharts() error {
	for entryFileName, entry := range c.Spec.Charts.Ytt {
		for valIndex, val := range entry.Values {
			valueTmpl, err := template.New("ytt entry value").Parse(val.Value)
			if err != nil {
				return fmt.Errorf("failed to parse ytt entry value as template: %q, %w", val.Value, err)
			}
			buf := new(bytes.Buffer)
			if err := valueTmpl.Execute(buf, c.prepareVariables(c.Spec.Variables)); err != nil {
				return fmt.Errorf("failed to hydrate ytt entry: %q, %w", val.Value, err)
			}
			// replace original content with hydrated version
			c.Spec.Charts.Ytt[entryFileName].Values[valIndex].Value = buf.String()
		}
	}
	return nil
}

func (c *CmdConfig) hydrateHelmCharts() error {
	for name, chart := range c.Spec.Charts.Helm {
		var newVals []string
		for _, value := range chart.Values {
			rawChartValue, err := yaml.Marshal(value)
			if err != nil {
				return fmt.Errorf("failed to get chart values as string: %w", err)
			}
			valueTmpl, err := template.New("chart").Parse(string(rawChartValue))
			if err != nil {
				return fmt.Errorf("failed to parse chart values as template: %q, %w", chart.Values, err)
			}
			buf := new(bytes.Buffer)
			if err := valueTmpl.Execute(buf, c.prepareVariables(c.Spec.Variables)); err != nil {
				return fmt.Errorf("failed to hydrate chart values entry: %q, %w", chart.Values, err)
			}
			newVals = append(newVals, buf.String())
		}
		chart.Values = newVals
		c.Spec.Charts.Helm[name] = 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)
}

func (c *CmdConfig) importCharts(other *Config) error {
	if err := c.importHelmCharts(other.Spec.Charts.Helm); err != nil {
		return nil
	}
	if err := c.importYttCharts(other.Spec.Charts.Ytt); err != nil {
		return nil
	}
	return nil
}

func (c *CmdConfig) importHelmCharts(helmCharts map[string]HelmChart) error {
	for id, chart := range helmCharts {
		convertedChart, err := cmdHelmChartFromHelmChart(chart)
		if err != nil {
			return err
		}
		_, ok := c.Spec.Charts.Helm[id]
		if !ok {
			// we have no chart by that name yet...
			// create one
			c.Spec.Charts.Helm[id] = *convertedChart
			continue
		}
		// else just append values to existing one
		convertedChart.Values = append(c.Spec.Charts.Helm[id].Values, convertedChart.Values...)
		c.Spec.Charts.Helm[id] = *convertedChart
	}
	return nil
}

func cmdHelmChartFromHelmChart(c HelmChart) (*CmdHelmChart, error) {
	strValues, err := yaml.Marshal(c.Values)
	if err != nil {
		return nil, err
	}
	return &CmdHelmChart{
		Type:   c.Type,
		Name:   c.Name,
		Values: []string{string(strValues)},
	}, nil
}

func (c *CmdConfig) importYttCharts(yttCharts map[string]YttChart) error {
	return nil
}