mirror of
https://github.com/foomo/squadron.git
synced 2025-10-16 12:35:42 +00:00
557 lines
16 KiB
Go
557 lines
16 KiB
Go
package squadron
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/foomo/squadron/internal/config"
|
|
templatex "github.com/foomo/squadron/internal/template"
|
|
"github.com/foomo/squadron/internal/util"
|
|
"github.com/miracl/conflate"
|
|
"github.com/pkg/errors"
|
|
"github.com/pterm/pterm"
|
|
"github.com/sergi/go-diff/diffmatchpatch"
|
|
"golang.org/x/sync/errgroup"
|
|
yamlv2 "gopkg.in/yaml.v2"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
errHelmReleaseNotFound = "Error: release: not found"
|
|
)
|
|
|
|
type Squadron struct {
|
|
basePath string
|
|
namespace string
|
|
files []string
|
|
config string
|
|
c config.Config
|
|
}
|
|
|
|
func New(basePath, namespace string, files []string) *Squadron {
|
|
return &Squadron{
|
|
basePath: basePath,
|
|
namespace: namespace,
|
|
files: files,
|
|
c: config.Config{},
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ~ Getter
|
|
// ------------------------------------------------------------------------------------------------
|
|
|
|
func (sq *Squadron) Namespace(ctx context.Context, squadron, unit string) (string, error) {
|
|
var out bytes.Buffer
|
|
t, err := template.New("namespace").Parse(sq.namespace)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := t.Execute(&out, map[string]string{"Squadron": squadron, "Unit": unit}); err != nil {
|
|
return "", err
|
|
}
|
|
return out.String(), nil
|
|
}
|
|
|
|
func (sq *Squadron) Config() config.Config {
|
|
return sq.c
|
|
}
|
|
|
|
func (sq *Squadron) ConfigYAML() string {
|
|
return sq.config
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------------------
|
|
// ~ Public methods
|
|
// ------------------------------------------------------------------------------------------------
|
|
|
|
func (sq *Squadron) MergeConfigFiles() error {
|
|
pterm.Debug.Println("merging config files")
|
|
pterm.Debug.Println(strings.Join(append([]string{"using files"}, sq.files...), "\n└ "))
|
|
|
|
mergedFiles, err := conflate.FromFiles(sq.files...)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to conflate files")
|
|
}
|
|
fileBytes, err := mergedFiles.MarshalYAML()
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to marshal yaml")
|
|
}
|
|
if err := yaml.Unmarshal(fileBytes, &sq.c); err != nil {
|
|
return err
|
|
}
|
|
if sq.c.Version != "2.0" {
|
|
return errors.New("Please upgrade your YAML definition to 2.0")
|
|
}
|
|
|
|
sq.c.Trim()
|
|
|
|
value, err := yamlv2.Marshal(sq.c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sq.config = string(value)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sq *Squadron) FilterConfig(squadron string, units []string) error {
|
|
if len(squadron) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err := sq.Config().Squadrons.Filter(squadron); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(squadron) > 0 && len(units) > 0 {
|
|
if err := sq.Config().Squadrons[squadron].Filter(units...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
value, err := yamlv2.Marshal(sq.c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sq.config = string(value)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sq *Squadron) RenderConfig(ctx context.Context) error {
|
|
pterm.Debug.Println("rendering config")
|
|
|
|
var tv templatex.Vars
|
|
var vars map[string]interface{}
|
|
if err := yaml.Unmarshal([]byte(sq.config), &vars); err != nil {
|
|
return err
|
|
}
|
|
// execute again with loaded template vars
|
|
tv = templatex.Vars{}
|
|
if value, ok := vars["global"]; ok {
|
|
util.ToSnakeCaseKeys(value)
|
|
tv.Add("Global", value)
|
|
}
|
|
if value, ok := vars["squadron"]; ok {
|
|
util.ToSnakeCaseKeys(value)
|
|
tv.Add("Squadron", value)
|
|
}
|
|
|
|
// execute without errors to get existing values
|
|
pterm.Debug.Println("executing file template")
|
|
// pterm.Debug.Println(sq.config)
|
|
out, err := templatex.ExecuteFileTemplate(ctx, sq.config, tv, false)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to execute initial file template\n%s", util.Highlight(sq.config))
|
|
}
|
|
|
|
// re-execute for rendering copied values
|
|
pterm.Debug.Println("re-executing file template")
|
|
// pterm.Debug.Println(string(out))
|
|
out, err = templatex.ExecuteFileTemplate(ctx, string(out), tv, false)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to re-execute initial file template")
|
|
}
|
|
|
|
pterm.Debug.Println("unmarshalling vars")
|
|
if err := yaml.Unmarshal(out, &vars); err != nil {
|
|
pterm.Error.Println(string(out))
|
|
return errors.Wrap(err, "failed to unmarshal vars")
|
|
}
|
|
|
|
// execute again with loaded template vars
|
|
tv = templatex.Vars{}
|
|
if value, ok := vars["global"]; ok {
|
|
util.ToSnakeCaseKeys(value)
|
|
tv.Add("Global", value)
|
|
}
|
|
if value, ok := vars["squadron"]; ok {
|
|
util.ToSnakeCaseKeys(value)
|
|
tv.Add("Squadron", value)
|
|
}
|
|
|
|
pterm.Debug.Println("executing file template")
|
|
out, err = templatex.ExecuteFileTemplate(ctx, sq.config, tv, true)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to execute second file template")
|
|
}
|
|
|
|
pterm.Debug.Println("unmarshalling vars")
|
|
if err := yaml.Unmarshal(out, &sq.c); err != nil {
|
|
pterm.Error.Println(string(out))
|
|
return errors.Wrap(err, "failed to unmarshal vars")
|
|
}
|
|
|
|
sq.config = string(out)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sq *Squadron) Push(ctx context.Context, pushArgs []string, parallel int) error {
|
|
wg, gctx := errgroup.WithContext(ctx)
|
|
wg.SetLimit(parallel)
|
|
|
|
_ = sq.Config().Squadrons.Iterate(func(key string, value config.Map[*config.Unit]) error {
|
|
return value.Iterate(func(k string, v *config.Unit) error {
|
|
wg.Go(func() error {
|
|
if out, err := v.Push(gctx, key, k, pushArgs); err != nil {
|
|
return errors.Wrap(err, out)
|
|
}
|
|
return nil
|
|
})
|
|
return nil
|
|
})
|
|
})
|
|
|
|
return wg.Wait()
|
|
}
|
|
|
|
func (sq *Squadron) Build(ctx context.Context, buildArgs []string, parallel int) error {
|
|
wg, gctx := errgroup.WithContext(ctx)
|
|
wg.SetLimit(parallel)
|
|
|
|
_ = sq.Config().Squadrons.Iterate(func(key string, value config.Map[*config.Unit]) error {
|
|
return value.Iterate(func(k string, v *config.Unit) error {
|
|
wg.Go(func() error {
|
|
if out, err := v.Build(gctx, key, k, buildArgs); err != nil {
|
|
return errors.Wrap(err, out)
|
|
}
|
|
return nil
|
|
})
|
|
return nil
|
|
})
|
|
})
|
|
|
|
return wg.Wait()
|
|
}
|
|
|
|
func (sq *Squadron) Down(ctx context.Context, helmArgs []string, parallel int) error {
|
|
wg, ctx := errgroup.WithContext(ctx)
|
|
wg.SetLimit(parallel)
|
|
|
|
_ = sq.Config().Squadrons.Iterate(func(key string, value config.Map[*config.Unit]) error {
|
|
return value.Iterate(func(k string, v *config.Unit) error {
|
|
wg.Go(func() error {
|
|
name := fmt.Sprintf("%s-%s", key, k)
|
|
namespace, err := sq.Namespace(ctx, key, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stdErr := bytes.NewBuffer([]byte{})
|
|
pterm.Debug.Printfln("running helm uninstall for: %s", name)
|
|
if out, err := util.NewHelmCommand().Args("uninstall", name).
|
|
Stderr(stdErr).
|
|
Stdout(os.Stdout).
|
|
Args("--namespace", namespace).
|
|
Args(helmArgs...).
|
|
Run(ctx); err != nil &&
|
|
string(bytes.TrimSpace(stdErr.Bytes())) != fmt.Sprintf("Error: uninstall: Release not loaded: %s: release: not found", name) {
|
|
return errors.Wrap(err, out)
|
|
}
|
|
return nil
|
|
})
|
|
return nil
|
|
})
|
|
})
|
|
|
|
return wg.Wait()
|
|
}
|
|
|
|
func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) error {
|
|
var diff string
|
|
wg, ctx := errgroup.WithContext(ctx)
|
|
wg.SetLimit(parallel)
|
|
|
|
_ = sq.Config().Squadrons.Iterate(func(key string, value config.Map[*config.Unit]) error {
|
|
return value.Iterate(func(k string, v *config.Unit) error {
|
|
wg.Go(func() error {
|
|
name := fmt.Sprintf("%s-%s", key, k)
|
|
namespace, err := sq.Namespace(ctx, key, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
valueBytes, err := v.ValuesYAML(sq.c.Global)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pterm.Debug.Printfln("running helm diff for: %s", k)
|
|
manifest, err := exec.CommandContext(ctx, "helm", "get", "manifest", name, "--namespace", sq.namespace).CombinedOutput()
|
|
if err != nil && string(bytes.TrimSpace(manifest)) != errHelmReleaseNotFound {
|
|
return errors.Wrap(err, string(manifest))
|
|
}
|
|
cmd := exec.CommandContext(ctx, "helm", "upgrade", name,
|
|
"--install",
|
|
"--namespace", namespace,
|
|
"--set", fmt.Sprintf("squadron=%s", key),
|
|
"--set", fmt.Sprintf("unit=%s", k),
|
|
"--values", "-",
|
|
"--dry-run",
|
|
)
|
|
cmd.Stdin = bytes.NewReader(valueBytes)
|
|
if strings.Contains(v.Chart.Repository, "file://") {
|
|
cmd.Args = append(cmd.Args, "/"+strings.TrimPrefix(v.Chart.Repository, "file://"))
|
|
} else {
|
|
cmd.Args = append(cmd.Args, v.Chart.Name, "--repo", v.Chart.Repository, "--version", v.Chart.Version)
|
|
}
|
|
cmd.Args = append(cmd.Args, helmArgs...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return errors.Wrap(err, string(out))
|
|
}
|
|
|
|
dmp := diffmatchpatch.New()
|
|
diff += dmp.DiffPrettyText(dmp.DiffMain(string(manifest), string(out), false))
|
|
return nil
|
|
})
|
|
return nil
|
|
})
|
|
})
|
|
|
|
if err := wg.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(diff)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int) error {
|
|
tbd := pterm.TableData{
|
|
{"Name", "Revision", "Status", "Deployed by", "Commit", "Branch", "Last deployed", "Notes"},
|
|
}
|
|
|
|
type statusType struct {
|
|
Name string `json:"name"`
|
|
Version int `json:"version"`
|
|
Namespace string `json:"namespace"`
|
|
Info struct {
|
|
Status string `json:"status"`
|
|
FirstDeployed string `json:"first_deployed"`
|
|
Deleted string `json:"deleted"`
|
|
LastDeployed string `json:"last_deployed"`
|
|
Description string `json:"description"`
|
|
} `json:"info"`
|
|
deployedBy string `json:"-"`
|
|
gitCommit string `json:"-"`
|
|
gitBranch string `json:"-"`
|
|
}
|
|
|
|
wg, ctx := errgroup.WithContext(ctx)
|
|
wg.SetLimit(parallel)
|
|
|
|
_ = sq.Config().Squadrons.Iterate(func(key string, value config.Map[*config.Unit]) error {
|
|
return value.Iterate(func(k string, v *config.Unit) error {
|
|
var status statusType
|
|
name := fmt.Sprintf("%s-%s", key, k)
|
|
namespace, err := sq.Namespace(ctx, key, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stdErr := bytes.NewBuffer([]byte{})
|
|
pterm.Debug.Printfln("running helm status for %s", name)
|
|
if out, err := util.NewHelmCommand().Args("status", name).
|
|
Stderr(stdErr).
|
|
Args("--namespace", namespace, "--output", "json", "--show-desc").
|
|
Args(helmArgs...).Run(ctx); err != nil && string(bytes.TrimSpace(stdErr.Bytes())) == errHelmReleaseNotFound {
|
|
tbd = append(tbd, []string{name, "0", "not installed", "", ""})
|
|
} else if err != nil {
|
|
return errors.Wrap(err, out)
|
|
} else if err := json.Unmarshal([]byte(out), &status); err != nil {
|
|
return errors.Wrap(err, out)
|
|
} else {
|
|
var notes []string
|
|
for _, line := range strings.Split(status.Info.Description, "\n") {
|
|
if strings.HasPrefix(line, "Managed-By: ") {
|
|
// do nothing
|
|
} else if strings.HasPrefix(line, "Deployed-By: ") {
|
|
status.deployedBy = strings.TrimPrefix(line, "Deployed-By: ")
|
|
} else if strings.HasPrefix(line, "Git-Commit: ") {
|
|
status.gitCommit = strings.TrimPrefix(line, "Git-Commit: ")
|
|
} else if strings.HasPrefix(line, "Git-Branch: ") {
|
|
status.gitBranch = strings.TrimPrefix(line, "Git-Branch: ")
|
|
} else {
|
|
notes = append(notes, line)
|
|
}
|
|
}
|
|
tbd = append(tbd, []string{
|
|
status.Name,
|
|
fmt.Sprintf("%d", status.Version),
|
|
status.Info.Status,
|
|
status.deployedBy,
|
|
status.gitCommit,
|
|
status.gitBranch,
|
|
status.Info.LastDeployed, strings.Join(notes, " | "),
|
|
})
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
|
|
if err := wg.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return pterm.DefaultTable.WithHasHeader().WithData(tbd).Render()
|
|
}
|
|
|
|
func (sq *Squadron) Rollback(ctx context.Context, revision string, helmArgs []string, parallel int) error {
|
|
if revision != "" {
|
|
helmArgs = append([]string{revision}, helmArgs...)
|
|
}
|
|
|
|
wg, ctx := errgroup.WithContext(ctx)
|
|
wg.SetLimit(parallel)
|
|
|
|
_ = sq.Config().Squadrons.Iterate(func(key string, value config.Map[*config.Unit]) error {
|
|
return value.Iterate(func(k string, v *config.Unit) error {
|
|
name := fmt.Sprintf("%s-%s", key, k)
|
|
namespace, err := sq.Namespace(ctx, key, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stdErr := bytes.NewBuffer([]byte{})
|
|
pterm.Debug.Printfln("running helm uninstall for: `%s`", name)
|
|
if out, err := util.NewHelmCommand().Args("rollback", name).
|
|
Stderr(stdErr).
|
|
Stdout(os.Stdout).
|
|
Args(helmArgs...).
|
|
Args("--namespace", namespace).
|
|
Run(ctx); err != nil &&
|
|
string(bytes.TrimSpace(stdErr.Bytes())) != fmt.Sprintf("Error: uninstall: Release not loaded: %s: release: not found", name) {
|
|
return errors.Wrap(err, out)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
return wg.Wait()
|
|
}
|
|
|
|
func (sq *Squadron) Up(ctx context.Context, helmArgs []string, username, version, commit, branch string, parallel int) error {
|
|
description := fmt.Sprintf("\nDeployed-By: %s\nManaged-By: Squadron %s\nGit-Commit: %s\nGit-Branch: %s", username, version, commit, branch)
|
|
|
|
wg, gctx := errgroup.WithContext(ctx)
|
|
wg.SetLimit(parallel)
|
|
|
|
_ = sq.Config().Squadrons.Iterate(func(key string, value config.Map[*config.Unit]) error {
|
|
return value.Iterate(func(k string, v *config.Unit) error {
|
|
wg.Go(func() error {
|
|
name := fmt.Sprintf("%s-%s", key, k)
|
|
namespace, err := sq.Namespace(ctx, key, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
valueBytes, err := v.ValuesYAML(sq.c.Global)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// update local chart dependencies
|
|
// https://stackoverflow.com/questions/59210148/error-found-in-chart-yaml-but-missing-in-charts-directory-mysql
|
|
if strings.HasPrefix(v.Chart.Repository, "file:///") {
|
|
pterm.Debug.Printfln("running helm dependency update for %s", v.Chart.Repository)
|
|
if out, err := util.NewHelmCommand().
|
|
Stdout(os.Stdout).
|
|
Args("dependency", "update").
|
|
Cwd(strings.TrimPrefix(v.Chart.Repository, "file://")).
|
|
Run(ctx); err != nil {
|
|
return errors.Wrap(err, out)
|
|
}
|
|
}
|
|
|
|
// install chart
|
|
pterm.Debug.Printfln("running helm upgrade for %s", name)
|
|
cmd := util.NewHelmCommand().
|
|
Stdin(bytes.NewReader(valueBytes)).
|
|
Stdout(os.Stdout).
|
|
Args("upgrade", name, "--install").
|
|
Args("--set", fmt.Sprintf("squadron=%s,unit=%s", key, k)).
|
|
Args("--description", description).
|
|
Args("--namespace", namespace).
|
|
Args("--dependency-update").
|
|
Args("--install").
|
|
Args("-values", "-").
|
|
Args(helmArgs...)
|
|
|
|
if strings.Contains(v.Chart.Repository, "file://") {
|
|
cmd.Args(strings.TrimPrefix(v.Chart.Repository, "file://"))
|
|
} else {
|
|
cmd.Args(v.Chart.Name, "--repo", v.Chart.Repository, "--version", v.Chart.Version)
|
|
}
|
|
|
|
if out, err := cmd.Run(gctx); err != nil {
|
|
return errors.Wrap(err, out)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
return nil
|
|
})
|
|
})
|
|
|
|
return wg.Wait()
|
|
}
|
|
|
|
func (sq *Squadron) Template(ctx context.Context, helmArgs []string) (string, error) {
|
|
var ret bytes.Buffer
|
|
|
|
err := sq.Config().Squadrons.Iterate(func(key string, value config.Map[*config.Unit]) error {
|
|
return value.Iterate(func(k string, v *config.Unit) error {
|
|
name := fmt.Sprintf("%s-%s", key, k)
|
|
namespace, err := sq.Namespace(ctx, key, k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
valueBytes, err := v.ValuesYAML(sq.c.Global)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pterm.Debug.Printfln("running helm template for chart: %s", name)
|
|
cmd := util.NewHelmCommand().Args("template", k).
|
|
Stdin(bytes.NewReader(valueBytes)).
|
|
Stdout(&ret).
|
|
Args("--dependency-update").
|
|
Args("--namespace", namespace).
|
|
Args("--set", fmt.Sprintf("squadron=%s", key)).
|
|
Args("--set", fmt.Sprintf("unit=%s", k)).
|
|
Args("--values", "-").
|
|
Args(helmArgs...)
|
|
if strings.Contains(v.Chart.Repository, "file://") {
|
|
file := strings.TrimPrefix(v.Chart.Repository, "file://")
|
|
if !strings.HasPrefix(file, ".") {
|
|
file = "/" + file
|
|
}
|
|
cmd.Args(file)
|
|
} else {
|
|
cmd.Args(v.Chart.Name, "--repo", v.Chart.Repository, "--version", v.Chart.Version)
|
|
}
|
|
if out, err := cmd.Run(ctx); err != nil {
|
|
return errors.Wrap(err, out)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return ret.String(), nil
|
|
}
|