-
steeve.chailloux authored
useful when using inherit
steeve.chailloux authoreduseful when using inherit
main.go 10.71 KiB
package runner
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/go-cmd/cmd"
"gopkg.in/yaml.v3"
)
var (
defaultFileMod os.FileMode = 0600
defaultDirMod os.FileMode = 0700
// TODO: find commands full path
yttCmd = "ytt"
helmCmd = "helm"
kubectlCmd = "kubectl"
)
// Runner is the struct in charge of launching commands
type Runner struct {
config *CmdConfig
}
// NewRunner ...
func NewRunner(cfg *CmdConfig) *Runner {
return &Runner{
config: cfg,
}
}
// Build is in charge of applying commands based on the config data
func (r *Runner) Build(tmpDir string) error {
var outputDir string
if r.config.Output == "" {
outputDir = filepath.Join(r.config.RootDir, "build", r.config.Namespace)
} else {
outputDir = r.config.Output
}
preBuildDir := filepath.Join(tmpDir, "pre-build")
if err := r.DoBuild(tmpDir, preBuildDir); err != nil {
return fmt.Errorf("failed to do pre-build: %w", err)
}
if err := r.config.SetShas(preBuildDir); err != nil {
return fmt.Errorf("failed to set SHAs: %w", err)
}
files, err := os.ReadDir(preBuildDir)
if err != nil {
return fmt.Errorf("cannot list directory: %s - %w", preBuildDir, err)
}
if err := CleanDir(outputDir); err != nil {
return fmt.Errorf("cannot clean dir: %s: %w", outputDir, err)
}
variables, err := r.config.prepareVariables(true)
if err != nil {
return fmt.Errorf("cannot prepare variables: %w", err)
}
for _, file := range files {
inFilePath := filepath.Join(preBuildDir, file.Name())
outFilePath := filepath.Join(outputDir, file.Name())
outFile, err := os.Create(outFilePath)
if err != nil {
return fmt.Errorf("cannot open: %s - %w", outFilePath, err)
}
defer func() {
if err := outFile.Close(); err != nil {
r.config.Logger.Fatal().Err(err).Msg("cannot close hydrated file")
}
}()
if err := hydrate(inFilePath, outFile, variables); err != nil {
return fmt.Errorf("cannot hydrate: %s - %w", outFilePath, err)
}
}
return nil
}
func (r *Runner) DoBuild(tmpDir, outputDir string) error {
cmds, err := r.prepareCmds()
if err != nil {
return err
}
compiled, err := r.runCommands(tmpDir, cmds)
if err != nil {
return err
}
yttOutput, err := r.runYtt(tmpDir, compiled)
if err != nil {
return err
}
kustomizeOutput, err := r.kustomize(tmpDir, yttOutput)
if err != nil {
return err
}
if r.config.DryRun {
return nil
}
if err := CleanDir(outputDir); err != nil {
return fmt.Errorf("cannot clean dir: %s: %w", outputDir, err)
}
if _, err := YamlSplit(outputDir, kustomizeOutput.Name()); err != nil {
return fmt.Errorf("cannot split full compiled file: %w", err)
}
return nil
}
func (r *Runner) kustomize(tmpDir string, input *os.File) (*os.File, error) {
kustomizeFilePath := filepath.Join(tmpDir, "kustomization.yaml")
f, err := os.Create(kustomizeFilePath)
if err != nil {
return nil, fmt.Errorf("fail to open %s: %w", kustomizeFilePath, err)
}
_, err = f.Write([]byte(fmt.Sprintf("resources: [%s]", filepath.Base(input.Name()))))
if err != nil {
return nil, fmt.Errorf("fail to write %s: %w", kustomizeFilePath, err)
}
variables, err := r.config.prepareVariables(false)
if err != nil {
return nil, fmt.Errorf("cannot prepare kustomize variables: %w", err)
}
var lastKustomizeFolder string
for _, layer := range r.config.Layers {
for _, ext := range []string{"yml", "yaml"} {
fName := fmt.Sprintf("kustomization.%s", ext)
fPath := filepath.Join(layer, "kustomize", fName)
fStat, err := os.Stat(fPath)
if err != nil || fStat.IsDir() {
continue
}
backupFile := fmt.Sprintf("%s.back", fPath)
if err := Copy(fPath, backupFile); err != nil {
return nil, fmt.Errorf("cannot copy kustomization file: %w", err)
}
defer func(fPath string) {
if err := Copy(backupFile, fPath); err != nil {
r.config.Logger.Fatal().Err(err).Msg("cannot restore kustomization back file")
}
if err := os.Remove(backupFile); err != nil {
r.config.Logger.Fatal().Err(err).Msg("cannot remove kustomization back file")
}
}(fPath)
if err := os.Remove(fPath); err != nil {
return nil, fmt.Errorf("cannot remove original kustomization file: %w", err)
}
outFile, err := os.Create(fPath)
if err != nil {
return nil, fmt.Errorf("cannot open: %s - %w", fPath, err)
}
defer func() {
if err := outFile.Close(); err != nil {
r.config.Logger.Fatal().Err(err).Msg("cannot close hydrated kustomization file")
}
}()
// kustomize root cannot be absolute
RelInputFilePath, err := filepath.Rel(filepath.Join(layer, "kustomize"), tmpDir)
if err != nil {
return nil, fmt.Errorf("cannot find relative path for: %s - %w", tmpDir, err)
}
variables["beaver.build"] = RelInputFilePath
if err := hydrate(backupFile, outFile, variables); err != nil {
return nil, fmt.Errorf("cannot hydrate: %s - %w", fPath, err)
}
lastKustomizeFolder = filepath.Join(layer, "kustomize")
}
}
// now run customize on the last layer with a kustomize folder
if lastKustomizeFolder != "" {
// now run customize on the last layer with a kustomize folder
kustomizeCmd := cmd.NewCmd(kubectlCmd, []string{"kustomize", lastKustomizeFolder}...)
return r.runCommand(tmpDir, "kustomize", kustomizeCmd)
}
return input, nil
}
func (r *Runner) prepareCmds() (map[string]*cmd.Cmd, error) {
// create helm commands
// create ytt chart commands
cmds := make(map[string]*cmd.Cmd)
for name, chart := range r.config.Spec.Charts {
if chart.Disabled {
continue
}
args, err := chart.BuildArgs(name, r.config.Namespace)
if err != nil {
return nil, fmt.Errorf("build: failed to build args %w", err)
}
switch chart.Type {
case HelmType:
cmds[name] = cmd.NewCmd(helmCmd, args...)
case YttType:
cmds[name] = cmd.NewCmd(yttCmd, args...)
default:
return nil, fmt.Errorf("unsupported chart %s type: %q", chart.Path, chart.Type)
}
}
for key, create := range r.config.Spec.Creates {
strArgs := key.BuildArgs(r.config.Namespace, create.Args)
name := fmt.Sprintf("%s_%s", key.Type, key.Name)
c := cmd.NewCmd(kubectlCmd, strArgs...)
c.Dir = create.Dir
cmds[name] = c
}
return cmds, nil
}
func (r *Runner) runCommand(tmpDir, name string, cmd *cmd.Cmd) (*os.File, error) {
tmpFile, err := os.CreateTemp(tmpDir, fmt.Sprintf("compiled-%s-*.yaml", name))
if err != nil {
return nil, fmt.Errorf("cannot create compiled file: %w", err)
}
defer func() {
if err := tmpFile.Close(); err != nil {
r.config.Logger.
Err(err).
Str("temp file", tmpFile.Name()).
Msg("failed to close temp file")
}
}()
if r.config.DryRun {
r.config.Logger.Info().
Str("command", cmd.Name).
Str("args", strings.Join(cmd.Args, " ")).
Msg("would run command")
return tmpFile, nil
}
stdOut, stdErr, err := RunCMD(cmd)
if err != nil {
r.config.Logger.Err(err).
Str("command", cmd.Name).
Str("args", strings.Join(cmd.Args, " ")).
Str("sdtout", strings.Join(stdOut, "\n")).
Str("stderr", strings.Join(stdErr, "\n")).
Msg("failed to run command")
// Error must be pretty printed to end users /!\
fmt.Printf("\n%s\n\n", strings.Join(stdErr, "\n"))
return nil, fmt.Errorf("failed to run command: %w", err)
}
if _, err := tmpFile.WriteString(strings.Join(stdOut, "\n")); err != nil {
return nil, fmt.Errorf("cannot write compiled file: %w", err)
}
return tmpFile, nil
}
func (r *Runner) runCommands(tmpDir string, cmds map[string]*cmd.Cmd) ([]string, error) {
var compiled []string
for name, cmd := range cmds {
f, err := r.runCommand(tmpDir, name, cmd)
if err != nil {
return nil, err
}
compiled = append(compiled, f.Name())
}
return compiled, nil
}
func (r *Runner) runYtt(tmpDir string, compiled []string) (*os.File, error) {
// create ytt additional command
args := r.config.BuildYttArgs(r.config.Spec.Ytt, compiled)
yttExtraCmd := cmd.NewCmd(yttCmd, args...)
return r.runCommand(tmpDir, "ytt", yttExtraCmd)
}
func CleanDir(directory string) error {
if err := os.RemoveAll(directory); err != nil {
return fmt.Errorf("cannot cleanup output directory: %w", err)
}
if err := os.MkdirAll(directory, defaultDirMod); err != nil {
return fmt.Errorf("cannot create output directory: %w", err)
}
return nil
}
func Copy(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Close()
}
// YamlSplit takes a buildDir and an inputFile.
// it returns a list of yaml documents and an eventual error
func YamlSplit(buildDir, inputFile string) ([]string, error) {
var docs []string
var allResources []map[string]interface{}
input, err := os.ReadFile(inputFile)
if err != nil {
return nil, err
}
if err := unmarshalAllResources(input, &allResources); err != nil {
return nil, err
}
for _, resource := range allResources {
apiVersionData, ok := resource["apiVersion"]
if !ok {
return nil, fmt.Errorf("apiVersion not present in resource: %+v", resource)
}
apiVersion, ok := apiVersionData.(string)
if !ok {
return nil, fmt.Errorf("failed to type assert apiVersion to string from: %+v", apiVersionData)
}
kind, ok := resource["kind"].(string)
if !ok {
return nil, fmt.Errorf("kind missing from: %+v", resource)
}
metadata, ok := resource["metadata"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("fail to type assert metadata from: %+v", resource)
}
name, ok := metadata["name"]
if !ok {
return nil, fmt.Errorf("fail to type get metadata.name from: %+v", resource)
}
filename := fmt.Sprintf("%s.%s.%s.yaml", kind, strings.ReplaceAll(apiVersion, "/", "_"), name)
fPath := filepath.Join(buildDir, filename)
buf := new(bytes.Buffer)
encoder := yaml.NewEncoder(buf)
encoder.SetIndent(2)
if err := encoder.Encode(resource); err != nil {
return nil, fmt.Errorf("cannot encode resource: %+v, %w", resource, err)
}
/*
out, err := yaml.Marshal(resource)
if err != nil {
return nil, fmt.Errorf("cannot marshal resource: %w", err)
}
*/
if err := os.MkdirAll(buildDir, defaultDirMod); err != nil {
return nil, fmt.Errorf("cannot create build directory: %w", err)
}
content := append([]byte("---\n"), buf.Bytes()...)
if err := os.WriteFile(fPath, content, defaultFileMod); err != nil {
return nil, fmt.Errorf("cannot write resource: %w", err)
}
docs = append(docs, fPath)
}
return docs, nil
}
func unmarshalAllResources(in []byte, out *[]map[string]interface{}) error {
r := bytes.NewReader(in)
decoder := yaml.NewDecoder(r)
for {
res := make(map[string]interface{})
if err := decoder.Decode(&res); err != nil {
// Break when there are no more documents to decode
if !errors.Is(err, io.EOF) {
return err
}
break
}
*out = append(*out, res)
}
return nil
}