Newer
Older
"strings"
"github.com/go-cmd/cmd"
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
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 := ioutil.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)
return fmt.Errorf("cannot open: %s - %w", outFilePath, err)
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)
}
func (r *Runner) DoBuild(tmpDir, outputDir string) error {
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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) {
cmds := make(map[string]*cmd.Cmd)
for name, chart := range r.config.Spec.Charts {
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...)
cmds[name] = cmd.NewCmd(yttCmd, args...)
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 := ioutil.TempFile(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")
}
}()
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)
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)
return r.runCommand(tmpDir, "ytt", yttExtraCmd)
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 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 {
apiVersion, ok := resource["apiVersion"].(string)
if !ok {
return nil, fmt.Errorf("fail to type assert apiVersion from: %+v", resource)
}
kind, ok := resource["kind"].(string)
if !ok {
return nil, fmt.Errorf("kind missing from: %+v", resource)
}
metadata, ok := resource["metadata"].(map[interface{}]interface{})
if !ok {
return nil, fmt.Errorf("fail to type assert metadata from: %+v", resource)
}
name, ok := metadata["name"].(string)
if !ok {
return nil, fmt.Errorf("fail to type assert metadata.name from: %+v", resource)
}
filename := fmt.Sprintf("%s.%s.%s.yaml", kind, strings.ReplaceAll(apiVersion, "/", "_"), name)
fPath := filepath.Join(buildDir, filename)
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"), out...)
if err := os.WriteFile(fPath, content, defaultFileMod); err != nil {
return nil, fmt.Errorf("cannot write resource: %w", err)
}
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
}