feat: parallel and better output

This commit is contained in:
Kevin Franklin Kim 2025-03-25 10:44:26 +01:00
parent 816d455c24
commit fa7ab36551
No known key found for this signature in database
32 changed files with 1328 additions and 677 deletions

View File

@ -4,47 +4,60 @@ import (
"github.com/foomo/squadron"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
buildCmd.Flags().BoolVarP(&flagPush, "push", "p", false, "pushes built squadron units to the registry")
buildCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel")
buildCmd.Flags().StringArrayVar(&flagBuildArgs, "build-args", nil, "additional docker buildx build args")
buildCmd.Flags().StringArrayVar(&flagPushArgs, "push-args", nil, "additional docker push args")
buildCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
func NewBuild(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "build [SQUADRON.UNIT...]",
Short: "build or rebuild squadron units",
Example: "squadron build storefinder frontend backend",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, "", c.GetStringSlice("file"))
var buildCmd = &cobra.Command{
Use: "build [SQUADRON.UNIT...]",
Short: "build or rebuild squadron units",
Example: " squadron build storefinder frontend backend",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, "", flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
if err := sq.Build(cmd.Context(), flagBuildArgs, flagParallel); err != nil {
return errors.Wrap(err, "failed to build units")
}
if flagPush {
if err := sq.Push(cmd.Context(), flagPushArgs, flagParallel); err != nil {
return errors.Wrap(err, "failed to push units")
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
}
return nil
},
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
if err := sq.Build(cmd.Context(), c.GetStringSlice("build-args"), c.GetInt("parallel")); err != nil {
return errors.Wrap(err, "failed to build units")
}
if c.GetBool("push") {
if err := sq.Push(cmd.Context(), c.GetStringSlice("push-args"), c.GetInt("parallel")); err != nil {
return errors.Wrap(err, "failed to push units")
}
}
return nil
},
}
flags := cmd.Flags()
flags.BoolP("push", "p", false, "pushes built squadron units to the registry")
_ = c.BindPFlag("push", flags.Lookup("push"))
cmd.Flags().Int("parallel", 1, "run command in parallel")
_ = c.BindPFlag("parallel", flags.Lookup("parallel"))
flags.StringArray("build-args", nil, "additional docker buildx build args")
_ = c.BindPFlag("build-args", flags.Lookup("build-args"))
flags.StringArray("push-args", nil, "additional docker push args")
_ = c.BindPFlag("push-args", flags.Lookup("push-args"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
return cmd
}

View File

@ -4,12 +4,14 @@ import (
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: `To load completions:
func NewCompletion(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: `To load completions:
Bash:
@ -48,20 +50,23 @@ PowerShell:
PS> squadron completion powershell > squadron.ps1
# and source this file from your PowerShell profile.
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
return nil
},
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
return nil
},
}
return cmd
}

View File

@ -1,44 +1,50 @@
package actions
import (
"fmt"
"github.com/foomo/squadron"
"github.com/foomo/squadron/internal/util"
"github.com/pkg/errors"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
configCmd.Flags().BoolVar(&flagNoRender, "no-render", false, "don't render the config template")
configCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
func NewConfig(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "config [SQUADRON] [UNIT...]",
Short: "generate and view the squadron config",
Example: " squadron config storefinder frontend backend",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, "", c.GetStringSlice("file"))
var configCmd = &cobra.Command{
Use: "config [SQUADRON] [UNIT...]",
Short: "generate and view the squadron config",
Example: " squadron config storefinder frontend backend",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, "", flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if !flagNoRender {
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
}
fmt.Print(util.Highlight(sq.ConfigYAML()))
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
return nil
},
if !c.GetBool("no-render") {
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
}
pterm.Println(util.Highlight(sq.ConfigYAML()))
return nil
},
}
flags := cmd.Flags()
flags.Bool("no-render", false, "don't render the config template")
_ = c.BindPFlag("no-render", flags.Lookup("no-render"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
return cmd
}

View File

@ -2,42 +2,60 @@ package actions
import (
"github.com/foomo/squadron"
"github.com/foomo/squadron/internal/util"
"github.com/pkg/errors"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
diffCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
diffCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel")
diffCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
var diffCmd = &cobra.Command{
Use: "diff [SQUADRON] [UNIT...]",
Short: "shows the diff between the installed and local chart",
Example: " squadron diff storefinder frontend backend --namespace demo",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, flagNamespace, flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
if err := sq.UpdateLocalDependencies(cmd.Context(), flagParallel); err != nil {
return errors.Wrap(err, "failed to update dependencies")
}
return sq.Diff(cmd.Context(), helmArgs, flagParallel)
},
func NewDiff(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "diff [SQUADRON] [UNIT...]",
Short: "shows the diff between the installed and local chart",
Example: " squadron diff storefinder frontend backend --namespace demo",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, c.GetString("namespace"), c.GetStringSlice("file"))
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
if err := sq.UpdateLocalDependencies(cmd.Context(), c.GetInt("parallel")); err != nil {
return errors.Wrap(err, "failed to update dependencies")
}
out, err := sq.Diff(cmd.Context(), helmArgs, c.GetInt("parallel"))
if err != nil {
return err
}
pterm.Println(util.Highlight(out))
return nil
},
}
flags := cmd.Flags()
flags.StringP("namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
_ = c.BindPFlag("namespace", flags.Lookup("namespace"))
flags.Int("parallel", 1, "run command in parallel")
_ = c.BindPFlag("parallel", flags.Lookup("parallel"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
return cmd
}

View File

@ -3,35 +3,44 @@ package actions
import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/foomo/squadron"
)
func init() {
downCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel")
downCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
downCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
var downCmd = &cobra.Command{
Use: "down [SQUADRON] [UNIT...]",
Short: "uninstalls the squadron or given units",
Example: " squadron down storefinder frontend backend --namespace demo",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, flagNamespace, flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
return sq.Down(cmd.Context(), helmArgs, flagParallel)
},
func NewDown(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "down [SQUADRON] [UNIT...]",
Short: "uninstalls the squadron or given units",
Example: " squadron down storefinder frontend backend --namespace demo",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, c.GetString("namespace"), c.GetStringSlice("file"))
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
return sq.Down(cmd.Context(), helmArgs, c.GetInt("parallel"))
},
}
flags := cmd.Flags()
flags.Int("parallel", 1, "run command in parallel")
_ = c.BindPFlag("parallel", flags.Lookup("parallel"))
flags.StringP("namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
_ = c.BindPFlag("namespace", flags.Lookup("namespace"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
return cmd
}

View File

@ -9,69 +9,75 @@ import (
"github.com/pterm/pterm"
"github.com/pterm/pterm/putils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
flagWithBuilds bool
flagWithCharts bool
flagWithTags bool
)
func NewList(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "list [SQUADRON]",
Short: "list squadron units",
Example: " squadron list storefinder",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, "", c.GetStringSlice("file"))
func init() {
listCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
listCmd.Flags().BoolVar(&flagWithTags, "with-tags", false, "include tags")
listCmd.Flags().BoolVar(&flagWithCharts, "with-charts", false, "include charts")
listCmd.Flags().BoolVar(&flagWithBuilds, "with-builds", false, "include builds")
}
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
var listCmd = &cobra.Command{
Use: "list [SQUADRON]",
Short: "list squadron units",
Example: " squadron list storefinder",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, "", flagFiles)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
var list pterm.LeveledList
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
var list pterm.LeveledList
// List squadrons
_ = sq.Config().Squadrons.Iterate(cmd.Context(), func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
list = append(list, pterm.LeveledListItem{Level: 0, Text: key})
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
list = append(list, pterm.LeveledListItem{Level: 1, Text: k})
if flagWithTags && len(v.Tags) > 0 {
list = append(list, pterm.LeveledListItem{Level: 2, Text: "🔖: " + v.Tags.SortedString()})
}
if flagWithCharts && len(v.Chart.String()) > 0 {
list = append(list, pterm.LeveledListItem{Level: 2, Text: "📑: " + v.Chart.String()})
}
if flagWithBuilds && len(v.Builds) > 0 {
for name, build := range v.Builds {
list = append(list, pterm.LeveledListItem{Level: 2, Text: "📦: " + name})
for _, dependency := range build.Dependencies {
list = append(list, pterm.LeveledListItem{Level: 3, Text: "🗃️: " + dependency})
// List squadrons
_ = sq.Config().Squadrons.Iterate(cmd.Context(), func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
list = append(list, pterm.LeveledListItem{Level: 0, Text: key})
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
list = append(list, pterm.LeveledListItem{Level: 1, Text: k})
if c.GetBool("with-tags") && len(v.Tags) > 0 {
list = append(list, pterm.LeveledListItem{Level: 2, Text: "🔖: " + v.Tags.SortedString()})
}
if c.GetBool("with-charts") && len(v.Chart.String()) > 0 {
list = append(list, pterm.LeveledListItem{Level: 2, Text: "📑: " + v.Chart.String()})
}
if c.GetBool("with-builds") && len(v.Builds) > 0 {
for name, build := range v.Builds {
list = append(list, pterm.LeveledListItem{Level: 2, Text: "📦: " + name})
for _, dependency := range build.Dependencies {
list = append(list, pterm.LeveledListItem{Level: 3, Text: "🗃️: " + dependency})
}
}
}
}
return nil
return nil
})
})
})
if len(list) > 0 {
root := putils.TreeFromLeveledList(list)
root.Text = "Squadron"
return pterm.DefaultTree.WithRoot(root).Render()
}
if len(list) > 0 {
root := putils.TreeFromLeveledList(list)
root.Text = "Squadron"
return pterm.DefaultTree.WithRoot(root).Render()
}
return nil
},
return nil
},
}
flags := cmd.Flags()
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
flags.Bool("with-tags", false, "include tags")
_ = c.BindPFlag("with-tags", flags.Lookup("with-tags"))
flags.Bool("with-charts", false, "include charts")
_ = c.BindPFlag("with-charts", flags.Lookup("with-charts"))
flags.Bool("with-builds", false, "include builds")
_ = c.BindPFlag("with-builds", flags.Lookup("with-builds"))
return cmd
}

View File

@ -7,31 +7,34 @@ import (
"path"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
}
var postRendererCmd = &cobra.Command{
Use: "post-renderer [PATH]",
Hidden: true,
Short: "render chart templates locally and display the output",
Example: " squadron template storefinder frontend backend --namespace demo",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
// this does the trick
r, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
return err
}
err = os.WriteFile(path.Join(args[0], ".chart.yaml"), r, 0600)
if err != nil {
return err
}
c := exec.CommandContext(cmd.Context(), "kustomize", "build", args[0])
c.Stdout = os.Stdout
return c.Run()
},
func NewPostRenderer(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "post-renderer [PATH]",
Hidden: true,
Short: "render chart templates locally and display the output",
Example: " squadron template storefinder frontend backend --namespace demo",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
// this does the trick
r, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
return err
}
err = os.WriteFile(path.Join(args[0], ".chart.yaml"), r, 0600)
if err != nil {
return err
}
c := exec.CommandContext(cmd.Context(), "kustomize", "build", args[0])
c.Stdout = os.Stdout
c.Stderr = os.Stderr
return c.Run()
},
}
return cmd
}

View File

@ -4,43 +4,58 @@ import (
"github.com/foomo/squadron"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
pushCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
pushCmd.Flags().BoolVarP(&flagBuild, "build", "b", false, "builds or rebuilds units")
pushCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel")
pushCmd.Flags().StringArrayVar(&flagBuildArgs, "build-args", nil, "additional docker buildx build args")
pushCmd.Flags().StringArrayVar(&flagPushArgs, "push-args", nil, "additional docker push args")
pushCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
func NewPush(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "push [SQUADRON] [UNIT...]",
Short: "pushes the squadron or given units",
Example: " squadron push storefinder frontend backend --namespace demo --build",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, c.GetString("namespace"), c.GetStringSlice("file"))
var pushCmd = &cobra.Command{
Use: "push [SQUADRON] [UNIT...]",
Short: "pushes the squadron or given units",
Example: " squadron push storefinder frontend backend --namespace demo --build",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, flagNamespace, flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
if flagBuild {
if err := sq.Build(cmd.Context(), flagBuildArgs, flagParallel); err != nil {
return errors.Wrap(err, "failed to build units")
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
}
return sq.Push(cmd.Context(), flagPushArgs, flagParallel)
},
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
if c.GetBool("build") {
if err := sq.Build(cmd.Context(), c.GetStringSlice("build-args"), c.GetInt("parallel")); err != nil {
return errors.Wrap(err, "failed to build units")
}
}
return sq.Push(cmd.Context(), c.GetStringSlice("push-args"), c.GetInt("parallel"))
},
}
flags := cmd.Flags()
flags.StringP("namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
_ = c.BindPFlag("namespace", flags.Lookup("namespace"))
flags.BoolP("build", "b", false, "builds or rebuilds units")
_ = c.BindPFlag("build", flags.Lookup("build"))
flags.Int("parallel", 1, "run command in parallel")
_ = c.BindPFlag("parallel", flags.Lookup("parallel"))
flags.StringArray("build-args", nil, "additional docker buildx build args")
_ = c.BindPFlag("build-args", flags.Lookup("build-args"))
flags.StringArray("push-args", nil, "additional docker push args")
_ = c.BindPFlag("push-args", flags.Lookup("push-args"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
return cmd
}

View File

@ -3,35 +3,46 @@ package actions
import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/foomo/squadron"
)
func init() {
rollbackCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel")
rollbackCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
rollbackCmd.Flags().StringVarP(&flagRevision, "revision", "r", "", "specifies the revision to roll back to")
rollbackCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
var rollbackCmd = &cobra.Command{
Use: "rollback [SQUADRON] [UNIT...]",
Short: "rolls back the squadron or given units",
Example: " squadron rollback storefinder frontend backend --namespace demo",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, flagNamespace, flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
return sq.Rollback(cmd.Context(), flagRevision, helmArgs, flagParallel)
},
func NewRollback(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "rollback [SQUADRON] [UNIT...]",
Short: "rolls back the squadron or given units",
Example: " squadron rollback storefinder frontend backend --namespace demo",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, c.GetString("namespace"), c.GetStringSlice("file"))
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
return sq.Rollback(cmd.Context(), c.GetString("revision"), helmArgs, c.GetInt("parallel"))
},
}
flags := cmd.Flags()
flags.Int("parallel", 1, "run command in parallel")
_ = c.BindPFlag("parallel", flags.Lookup("parallel"))
flags.StringP("namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
_ = c.BindPFlag("namespace", flags.Lookup("namespace"))
flags.StringP("revision", "r", "", "specifies the revision to roll back to")
_ = c.BindPFlag("revision", flags.Lookup("revision"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("namespace"))
return cmd
}

View File

@ -1,75 +1,103 @@
package actions
import (
"fmt"
"os"
"runtime/debug"
"strings"
cowsay "github.com/Code-Hex/Neo-cowsay/v2"
"github.com/foomo/squadron/internal/cmd"
"github.com/foomo/squadron/internal/util"
"github.com/pkg/errors"
"github.com/pterm/pterm"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
rootCmd = &cobra.Command{
cwd string
root *cobra.Command
)
func init() {
root = NewRoot()
root.AddCommand(
NewUp(NewViper(root)),
NewDiff(NewViper(root)),
NewDown(NewViper(root)),
NewBuild(NewViper(root)),
NewPush(NewViper(root)),
NewList(NewViper(root)),
NewRollback(NewViper(root)),
NewStatus(NewViper(root)),
NewConfig(NewViper(root)),
NewVersion(NewViper(root)),
NewCompletion(NewViper(root)),
NewTemplate(NewViper(root)),
NewPostRenderer(NewViper(root)),
NewSchema(NewViper(root)),
)
}
// NewRoot represents the base command when called without any subcommands
func NewRoot() *cobra.Command {
root := &cobra.Command{
Use: "squadron",
Short: "Docker compose for kubernetes",
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if flagSilent {
logrus.SetLevel(logrus.ErrorLevel)
} else if flagDebug {
logrus.SetLevel(logrus.TraceLevel)
if viper.GetBool("debug") {
pterm.EnableDebugMessages()
} else if flagVerbose {
logrus.SetLevel(logrus.InfoLevel)
} else {
logrus.SetLevel(logrus.WarnLevel)
}
if cmd.Name() == "help" || cmd.Name() == "init" || cmd.Name() == "version" {
return nil
}
// cwd
return util.ValidatePath(".", &cwd)
},
}
cwd string
flagSilent bool
flagDebug bool
flagVerbose bool
flagNoRender bool
flagNamespace string
flagRevision string
flagBuild bool
flagPush bool
flagParallel int
flagBuildArgs []string
flagPushArgs []string
flagTags []string
flagFiles []string
)
flags := root.PersistentFlags()
flags.BoolP("debug", "d", false, "show all output")
_ = viper.BindPFlag("debug", root.PersistentFlags().Lookup("debug"))
func init() {
rootCmd.PersistentFlags().BoolVarP(&flagSilent, "silent", "s", false, "only show errors")
rootCmd.PersistentFlags().BoolVarP(&flagDebug, "debug", "d", false, "show all output")
rootCmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "show more output")
rootCmd.PersistentFlags().StringSliceVarP(&flagFiles, "file", "f", []string{"squadron.yaml"}, "specify alternative squadron files")
flags.StringSliceP("file", "f", []string{"squadron.yaml"}, "specify alternative squadron files")
rootCmd.AddCommand(upCmd, diffCmd, downCmd, buildCmd, pushCmd, listCmd, rollbackCmd, statusCmd, configCmd, versionCmd, completionCmd, templateCmd, postRendererCmd, schemaCmd)
return root
}
pterm.Info = *pterm.Info.WithPrefix(pterm.Prefix{Text: "⎈", Style: pterm.Info.Prefix.Style})
pterm.Debug = *pterm.Debug.WithPrefix(pterm.Prefix{Text: "⚒︎", Style: pterm.Debug.Prefix.Style})
pterm.Fatal = *pterm.Fatal.WithPrefix(pterm.Prefix{Text: "💀", Style: pterm.Fatal.Prefix.Style})
pterm.Error = *pterm.Error.WithPrefix(pterm.Prefix{Text: "⛌", Style: pterm.Error.Prefix.Style})
pterm.Warning = *pterm.Info.WithPrefix(pterm.Prefix{Text: "⚠", Style: pterm.Warning.Prefix.Style})
pterm.Success = *pterm.Success.WithPrefix(pterm.Prefix{Text: "✓", Style: pterm.Success.Prefix.Style})
func NewViper(root *cobra.Command) *viper.Viper {
c := viper.New()
_ = c.BindPFlag("file", root.PersistentFlags().Lookup("file"))
return c
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
pterm.Error.Println(err.Error())
os.Exit(1)
l := cmd.NewLogger()
say := func(msg string) string {
if say, cerr := cowsay.Say(msg, cowsay.BallonWidth(80)); cerr == nil {
msg = say
}
return msg
}
code := 0
defer func() {
if r := recover(); r != nil {
l.Error(say("It's time to panic"))
l.Error(fmt.Sprintf("%v", r))
l.Error(string(debug.Stack()))
code = 1
}
os.Exit(code)
}()
if err := root.Execute(); err != nil {
l.Error(say(strings.Split(errors.Cause(err).Error(), ":")[0]))
l.Error(util.SprintError(err))
code = 1
}
}

View File

@ -1,7 +1,6 @@
package actions
import (
"fmt"
"os"
"github.com/foomo/squadron"
@ -9,50 +8,54 @@ import (
"github.com/pkg/errors"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
flagOutput string
flagBaseSchema string
)
func NewSchema(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "schema [SQUADRON]",
Short: "generate squadron json schema",
Example: " squadron schema",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, "", c.GetStringSlice("file"))
func init() {
schemaCmd.Flags().StringVar(&flagOutput, "output", "", "Output file")
schemaCmd.Flags().StringVar(&flagBaseSchema, "base-schema", "https://raw.githubusercontent.com/foomo/squadron/refs/heads/main/squadron.schema.json", "Base schema to use")
schemaCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
var schemaCmd = &cobra.Command{
Use: "schema [SQUADRON]",
Short: "generate squadron json schema",
Example: " squadron schema",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, "", flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
js, err := sq.RenderSchema(cmd.Context(), flagBaseSchema)
if err != nil {
return errors.Wrap(err, "failed to render schema")
}
if flagOutput != "" {
pterm.Info.Printfln("Writing JSON schema to %s", flagOutput)
if err := os.WriteFile(flagOutput, []byte(js), 0600); err != nil {
return errors.Wrap(err, "failed to write schema")
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
} else {
fmt.Print(util.Highlight(js))
}
return nil
},
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
js, err := sq.RenderSchema(cmd.Context(), c.GetString("base-schema"))
if err != nil {
return errors.Wrap(err, "failed to render schema")
}
if output := c.GetString("output"); output != "" {
pterm.Info.Printfln("Writing JSON schema to %s", output)
if err := os.WriteFile(output, []byte(js), 0600); err != nil {
return errors.Wrap(err, "failed to write schema")
}
} else {
pterm.Println(util.Highlight(js))
}
return nil
},
}
flags := cmd.Flags()
flags.String("output", "", "Output file")
_ = c.BindPFlag("output", flags.Lookup("output"))
flags.String("base-schema", "https://raw.githubusercontent.com/foomo/squadron/refs/heads/main/squadron.schema.json", "Base schema to use")
_ = c.BindPFlag("base-schema", flags.Lookup("base-schema"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
return cmd
}

View File

@ -3,34 +3,43 @@ package actions
import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/foomo/squadron"
)
func init() {
statusCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel")
statusCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
statusCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
var statusCmd = &cobra.Command{
Use: "status [SQUADRON] [UNIT...]",
Short: "installs the squadron or given units",
Example: " squadron status storefinder frontend backend --namespace demo",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, flagNamespace, flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
return sq.Status(cmd.Context(), helmArgs, flagParallel)
},
func NewStatus(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "status [SQUADRON] [UNIT...]",
Short: "installs the squadron or given units",
Example: " squadron status storefinder frontend backend --namespace demo",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, c.GetString("namespace"), c.GetStringSlice("file"))
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
return sq.Status(cmd.Context(), helmArgs, c.GetInt("parallel"))
},
}
flags := cmd.Flags()
flags.Int("parallel", 1, "run command in parallel")
_ = c.BindPFlag("parallel", flags.Lookup("parallel"))
flags.StringP("namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
_ = c.BindPFlag("namespace", flags.Lookup("namespace"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
return cmd
}

View File

@ -1,54 +1,62 @@
package actions
import (
"fmt"
"github.com/foomo/squadron"
"github.com/foomo/squadron/internal/util"
"github.com/pkg/errors"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
templateCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel")
templateCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
templateCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
var templateCmd = &cobra.Command{
Use: "template [SQUADRON] [UNIT...]",
Short: "render chart templates locally and display the output",
Example: " squadron template storefinder frontend backend --namespace demo",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, flagNamespace, flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
if err := sq.UpdateLocalDependencies(cmd.Context(), flagParallel); err != nil {
return errors.Wrap(err, "failed to update dependencies")
}
out, err := sq.Template(cmd.Context(), helmArgs, flagParallel)
if err != nil {
return errors.Wrap(err, "failed to render template")
}
fmt.Print(util.Highlight(out))
return nil
},
func NewTemplate(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "template [SQUADRON] [UNIT...]",
Short: "render chart templates locally and display the output",
Example: " squadron template storefinder frontend backend --namespace demo",
Args: cobra.MinimumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, c.GetString("namespace"), c.GetStringSlice("file"))
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
if err := sq.UpdateLocalDependencies(cmd.Context(), c.GetInt("parallel")); err != nil {
return errors.Wrap(err, "failed to update dependencies")
}
out, err := sq.Template(cmd.Context(), helmArgs, c.GetInt("parallel"))
if err != nil {
return errors.Wrap(err, "failed to render template")
}
pterm.Println(util.Highlight(out))
return nil
},
}
flags := cmd.Flags()
flags.Int("parallel", 1, "run command in parallel")
_ = c.BindPFlag("parallel", flags.Lookup("parallel"))
flags.StringP("namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
_ = c.BindPFlag("namespace", flags.Lookup("namespace"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
return cmd
}

View File

@ -8,72 +8,89 @@ import (
"github.com/foomo/squadron/internal/util"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func init() {
upCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
upCmd.Flags().BoolVarP(&flagBuild, "build", "b", false, "builds or rebuilds units")
upCmd.Flags().BoolVarP(&flagPush, "push", "p", false, "pushes units to the registry")
upCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel")
upCmd.Flags().StringArrayVar(&flagBuildArgs, "build-args", nil, "additional docker buildx build args")
upCmd.Flags().StringArrayVar(&flagPushArgs, "push-args", nil, "additional docker push args")
upCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
}
func NewUp(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "up [SQUADRON] [UNIT...]",
Short: "installs the squadron or given units",
Example: " squadron up storefinder frontend backend --namespace demo --build --push -- --dry-run",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, c.GetString("namespace"), c.GetStringSlice("file"))
var upCmd = &cobra.Command{
Use: "up [SQUADRON] [UNIT...]",
Short: "installs the squadron or given units",
Example: " squadron up storefinder frontend backend --namespace demo --build --push -- --dry-run",
RunE: func(cmd *cobra.Command, args []string) error {
sq := squadron.New(cwd, flagNamespace, flagFiles)
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, flagTags); err != nil {
return errors.Wrap(err, "failed to filter config")
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
if flagBuild {
if err := sq.Build(cmd.Context(), flagBuildArgs, flagParallel); err != nil {
return errors.Wrap(err, "failed to build units")
if err := sq.MergeConfigFiles(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to merge config files")
}
}
if flagPush {
if err := sq.Push(cmd.Context(), flagPushArgs, flagParallel); err != nil {
return errors.Wrap(err, "failed to push units")
args, helmArgs := parseExtraArgs(args)
squadronName, unitNames := parseSquadronAndUnitNames(args)
if err := sq.FilterConfig(cmd.Context(), squadronName, unitNames, c.GetStringSlice("tags")); err != nil {
return errors.Wrap(err, "failed to filter config")
}
}
if err := sq.UpdateLocalDependencies(cmd.Context(), flagParallel); err != nil {
return err
}
if err := sq.RenderConfig(cmd.Context()); err != nil {
return errors.Wrap(err, "failed to render config")
}
username := "unknown"
if value, err := util.NewCommand("git").Args("config", "user.name").Run(cmd.Context()); err == nil {
username = strings.TrimSpace(value)
} else if value, err := user.Current(); err == nil {
username = strings.TrimSpace(value.Name)
}
if c.GetBool("build") {
if err := sq.Build(cmd.Context(), c.GetStringSlice("build-args"), c.GetInt("parallel")); err != nil {
return errors.Wrap(err, "failed to build units")
}
}
branch := ""
if value, err := util.NewCommand("sh").Args("-c", "git describe --tags --exact-match 2> /dev/null || git symbolic-ref -q --short HEAD || git rev-parse --short HEAD").Run(cmd.Context()); err == nil {
branch = strings.TrimSpace(value)
}
commit := ""
if value, err := util.NewCommand("sh").Args("-c", "git rev-parse --short HEAD").Run(cmd.Context()); err == nil {
commit = strings.TrimSpace(value)
}
if c.GetBool("push") {
if err := sq.Push(cmd.Context(), c.GetStringSlice("push-args"), c.GetInt("parallel")); err != nil {
return errors.Wrap(err, "failed to push units")
}
}
return sq.Up(cmd.Context(), helmArgs, username, version, commit, branch, flagParallel)
},
if err := sq.UpdateLocalDependencies(cmd.Context(), c.GetInt("parallel")); err != nil {
return err
}
username := "unknown"
if value, err := util.NewCommand("git").Args("config", "user.name").Run(cmd.Context()); err == nil {
username = strings.TrimSpace(value)
} else if value, err := user.Current(); err == nil {
username = strings.TrimSpace(value.Name)
}
branch := ""
if value, err := util.NewCommand("sh").Args("-c", "git describe --tags --exact-match 2> /dev/null || git symbolic-ref -q --short HEAD || git rev-parse --short HEAD").Run(cmd.Context()); err == nil {
branch = strings.TrimSpace(value)
}
commit := ""
if value, err := util.NewCommand("sh").Args("-c", "git rev-parse --short HEAD").Run(cmd.Context()); err == nil {
commit = strings.TrimSpace(value)
}
return sq.Up(cmd.Context(), helmArgs, username, version, commit, branch, c.GetInt("parallel"))
},
}
flags := cmd.Flags()
flags.StringP("namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})")
_ = c.BindPFlag("namespace", flags.Lookup("namespace"))
flags.BoolP("build", "b", false, "builds or rebuilds units")
_ = c.BindPFlag("build", flags.Lookup("build"))
flags.BoolP("push", "p", false, "pushes units to the registry")
_ = c.BindPFlag("push", flags.Lookup("push"))
flags.Int("parallel", 1, "run command in parallel")
_ = c.BindPFlag("parallel", flags.Lookup("parallel"))
flags.StringArray("build-args", nil, "additional docker buildx build args")
_ = c.BindPFlag("build-args", flags.Lookup("build-args"))
flags.StringArray("push-args", nil, "additional docker push args")
_ = c.BindPFlag("push-args", flags.Lookup("push-args"))
flags.StringSlice("tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)")
_ = c.BindPFlag("tags", flags.Lookup("tags"))
return cmd
}

View File

@ -1,17 +1,20 @@
package actions
import (
"fmt"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var version = "latest"
var versionCmd = &cobra.Command{
Use: "version",
Short: "show version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(version)
},
func NewVersion(c *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "show version information",
Run: func(cmd *cobra.Command, args []string) {
pterm.Println(version)
},
}
return cmd
}

36
internal/cmd/logger.go Normal file
View File

@ -0,0 +1,36 @@
package cmd
import (
"log/slog"
"os"
"github.com/pterm/pterm"
)
func init() {
pterm.Info.Prefix.Text = "⎈"
pterm.Info.Scope.Style = &pterm.ThemeDefault.DebugMessageStyle
pterm.Debug.Prefix.Text = "⛏︎"
pterm.Debug.Scope.Style = &pterm.ThemeDefault.DebugMessageStyle
pterm.Fatal.Prefix.Text = "⛔︎"
pterm.Fatal.Scope.Style = &pterm.ThemeDefault.DebugMessageStyle
pterm.Error.Prefix.Text = "⛌"
pterm.Error.Scope.Style = &pterm.ThemeDefault.DebugMessageStyle
pterm.Warning.Prefix.Text = "⚠"
pterm.Warning.Scope.Style = &pterm.ThemeDefault.DebugMessageStyle
pterm.Success.Prefix.Text = "✓"
pterm.Success.Scope.Style = &pterm.ThemeDefault.DebugMessageStyle
if scope := os.Getenv("SQUADRON_SCOPE"); scope != "" {
pterm.Info.Scope.Text = scope
pterm.Debug.Scope.Text = scope
pterm.Fatal.Scope.Text = scope
pterm.Error.Scope.Text = scope
pterm.Warning.Scope.Text = scope
pterm.Success.Scope.Text = scope
}
}
func NewLogger() *slog.Logger {
return slog.New(NewPTermSlogHandler())
}

View File

@ -0,0 +1,85 @@
package cmd
import (
"context"
"fmt"
"log/slog"
"github.com/pterm/pterm"
)
type PTermSlogHandler struct {
attrs []slog.Attr
}
// Enabled returns true if the given level is enabled.
func (s *PTermSlogHandler) Enabled(ctx context.Context, level slog.Level) bool {
switch level {
case slog.LevelDebug:
return pterm.PrintDebugMessages
default:
return true
}
}
// Handle handles the given record.
func (s *PTermSlogHandler) Handle(ctx context.Context, record slog.Record) error {
level := record.Level
message := record.Message
// Convert slog Attrs to a map.
keyValsMap := make(map[string]any)
record.Attrs(func(attr slog.Attr) bool {
keyValsMap[attr.Key] = attr.Value
return true
})
for _, attr := range s.attrs {
keyValsMap[attr.Key] = attr.Value
}
args := pterm.DefaultLogger.ArgsFromMap(keyValsMap)
// Wrapping args inside another slice to match [][]LoggerArgument
argsWrapped := [][]pterm.LoggerArgument{args}
for _, arg := range argsWrapped {
for _, attr := range arg {
message += " " + attr.Key + ": " + fmt.Sprintf("%v", attr.Value)
}
}
switch level {
case slog.LevelDebug:
pterm.Debug.Println(message)
case slog.LevelInfo:
pterm.Info.Println(message)
case slog.LevelWarn:
pterm.Warning.Println(message)
case slog.LevelError:
pterm.Error.Println(message)
default:
pterm.Info.Println(message)
}
return nil
}
// WithAttrs returns a new handler with the given attributes.
func (s *PTermSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
newS := *s
newS.attrs = attrs
return &newS
}
// WithGroup is not yet supported.
func (s *PTermSlogHandler) WithGroup(name string) slog.Handler {
// Grouping is not yet supported by pterm.
return s
}
// NewPTermSlogHandler returns a new logging handler that can be intrgrated with log/slog.
func NewPTermSlogHandler() *PTermSlogHandler {
return &PTermSlogHandler{}
}

View File

@ -27,7 +27,7 @@ func (m Map[T]) Trim() {
continue
}
switch val.Kind() {
switch val.Kind() { //nolint:exhaustive
case reflect.Map, reflect.Slice:
if val.Len() == 0 {
delete(m, key)

View File

@ -5,6 +5,8 @@ import (
"encoding/json"
"path"
"strings"
"github.com/pkg/errors"
)
// JSONSchema represents the structure of a JSON schema
@ -50,7 +52,7 @@ func (js *JSONSchema) SetSquadronUnitSchema(ctx context.Context, squardon, unit,
if _, ok := defsMap[ref]; !ok {
valuesMap, err := LoadMap(ctx, url)
if err != nil {
return err
return errors.Wrap(err, "failed to load map: "+url)
}
delete(valuesMap, "$schema")
js.ensure(defsMap, ref, valuesMap)

View File

@ -1,7 +1,6 @@
package jsonschema_test
import (
"context"
"fmt"
"testing"
@ -17,11 +16,11 @@ func TestJSONSchema(t *testing.T) {
// Create the JSONSchema object
js := jsonschema.New()
err := js.LoadBaseSchema(context.TODO(), baseURL)
err := js.LoadBaseSchema(t.Context(), baseURL)
require.NoError(t, err)
// Override the base schema
err = js.SetSquadronUnitSchema(context.TODO(), "site", "namespace", overrideURL)
err = js.SetSquadronUnitSchema(t.Context(), "site", "namespace", overrideURL)
require.NoError(t, err)
// Print the resulting schema

View File

@ -7,6 +7,8 @@ import (
"net/http"
"os"
"strings"
"github.com/pterm/pterm"
)
// LoadMap fetches the JSON schema from a given URL
@ -15,6 +17,7 @@ func LoadMap(ctx context.Context, url string) (map[string]any, error) {
var body []byte
if strings.HasPrefix(url, "http") {
pterm.Debug.Printfln("Loading map from %s", url)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err

View File

@ -1,7 +1,6 @@
package jsonschema_test
import (
"context"
"testing"
"github.com/foomo/squadron/internal/jsonschema"
@ -10,7 +9,7 @@ import (
)
func TestLoadMap(t *testing.T) {
actual, err := jsonschema.LoadMap(context.TODO(), "https://raw.githubusercontent.com/foomo/squadron/refs/heads/main/squadron.schema.json")
actual, err := jsonschema.LoadMap(t.Context(), "https://raw.githubusercontent.com/foomo/squadron/refs/heads/main/squadron.schema.json")
require.NoError(t, err)
assert.NotNil(t, actual)
assert.Equal(t, "https://github.com/foomo/squadron/internal/config/config", actual["$id"])

View File

@ -16,7 +16,7 @@ func Snapshot(t *testing.T, name, yaml string) {
if *UpdateFlag || snapshot == "" {
writeSnapshot(t, name, yaml)
}
assert.Equal(t, snapshot, yaml)
assert.YAMLEq(t, snapshot, yaml)
}
// writeSnapshot updates the snapshot file for a given test t.

View File

@ -7,8 +7,8 @@ import (
"os"
"os/exec"
"github.com/pkg/errors"
"github.com/pterm/pterm"
"github.com/sirupsen/logrus"
)
type Cmd struct {
@ -126,19 +126,26 @@ func (c *Cmd) Run(ctx context.Context) (string, error) {
if c.cwd != "" {
cmd.Dir = c.cwd
}
pterm.Debug.Printfln("executing %s", cmd.String())
var stdout bytes.Buffer
var stderr bytes.Buffer
traceWriter := logrus.StandardLogger().WriterLevel(logrus.TraceLevel)
if c.stdin != nil {
cmd.Stdin = c.stdin
}
cmd.Stdout = io.MultiWriter(append(c.stdoutWriters, &stdout, traceWriter)...)
cmd.Stderr = io.MultiWriter(append(c.stderrWriters, &stderr, traceWriter)...)
if value := PTermSpinnerFromContext(ctx); value != nil {
c.stdoutWriters = append(c.stdoutWriters, value)
c.stderrWriters = append(c.stderrWriters, value)
}
cmd.Stdout = io.MultiWriter(append(c.stdoutWriters, &stdout)...)
cmd.Stderr = io.MultiWriter(append(c.stderrWriters, &stderr)...)
err := cmd.Run()
if err != nil {
err = errors.Wrap(err, "failed to execute: "+cmd.String())
}
return stdout.String() + stderr.String(), err
}

32
internal/util/errors.go Normal file
View File

@ -0,0 +1,32 @@
package util
import (
"errors"
"fmt"
"strings"
"github.com/pterm/pterm"
)
func SprintError(err error) string {
var ret string
prefix := "Error: "
if pterm.PrintDebugMessages {
return fmt.Sprintf("%+v", err) + "\n"
}
for {
w := errors.Unwrap(err)
if w == nil {
ret += prefix + err.Error() + "\n"
break
}
if err.Error() != w.Error() {
ret += prefix + strings.TrimSuffix(err.Error(), ": "+w.Error()) + "\n"
prefix = "↪ "
}
err = w
}
return strings.TrimSuffix(ret, "\n")
}

View File

@ -0,0 +1,22 @@
package util_test
import (
"errors"
"strings"
"testing"
"github.com/foomo/squadron/internal/util"
errorsx "github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestSprintError(t *testing.T) {
err := errors.New("test error")
err = errorsx.Wrap(err, "1 wrap")
err = errorsx.WithMessage(err, "2 with message")
ret := util.SprintError(err)
t.Log(ret)
assert.Len(t, strings.Split(ret, "\n"), 3)
}

View File

@ -7,6 +7,7 @@ import (
"os"
"strings"
"github.com/pkg/errors"
k8s "k8s.io/api/apps/v1"
)
@ -45,7 +46,7 @@ func (c KubeCmd) GetMostRecentPodBySelectors(ctx context.Context, selectors map[
if len(pods) > 0 {
return pods[len(pods)-1], nil
}
return "", fmt.Errorf("no pods found")
return "", errors.New("no pods found")
}
func (c KubeCmd) WaitForPodState(pod, condition, timeout string) *Cmd {

View File

@ -0,0 +1,39 @@
package util
import (
"os"
"github.com/pterm/pterm"
)
type PTermMultiPrinter struct {
printer *pterm.MultiPrinter
}
func MustNewPTermMultiPrinter() *PTermMultiPrinter {
printer, err := NewPTermMultiPrinter()
if err != nil {
pterm.Fatal.Println(err)
}
return printer
}
func NewPTermMultiPrinter() (*PTermMultiPrinter, error) {
printer, err := pterm.DefaultMultiPrinter.WithWriter(os.Stdout).Start()
if err != nil {
return nil, err
}
return &PTermMultiPrinter{printer: printer}, nil
}
func (s *PTermMultiPrinter) NewSpinner(prefix string) *PTermSpinner {
return NewPTermSpinner(s.printer.NewWriter(), prefix)
}
func (s *PTermMultiPrinter) Stop() {
if s.printer.IsActive {
if _, err := s.printer.Stop(); err != nil {
pterm.Fatal.Println(err)
}
}
}

View File

@ -0,0 +1,97 @@
package util
import (
"context"
"io"
"strings"
"time"
"github.com/pterm/pterm"
)
type contextKey string
const contextKeyPTermSpinner contextKey = "PtermSpinner"
type PTermSpinner struct {
printer *pterm.SpinnerPrinter
prefix string
stopped bool
start time.Time
log []string
}
func NewPTermSpinner(writer io.Writer, prefix string) *PTermSpinner {
return &PTermSpinner{
printer: pterm.DefaultSpinner.WithWriter(writer).
WithDelay(500*time.Millisecond).
WithSequence("▀ ", " ▀ ", " ▄ ", "▄ ").
WithShowTimer(false),
prefix: prefix,
}
}
func PTermSpinnerFromContext(ctx context.Context) *PTermSpinner {
if value, ok := ctx.Value(contextKeyPTermSpinner).(*PTermSpinner); ok {
return value
}
return nil
}
func (s *PTermSpinner) Inject(ctx context.Context) context.Context {
return context.WithValue(ctx, contextKeyPTermSpinner, s)
}
func (s *PTermSpinner) Start(message ...string) {
var err error
if s.printer, err = s.printer.Start(s.message(message...)); err != nil {
pterm.Fatal.Println(err)
}
s.start = time.Now()
}
func (s *PTermSpinner) Info(message ...string) {
s.stopped = true
s.printer.Info(s.message(message...))
}
func (s *PTermSpinner) Warning(message ...string) {
s.stopped = true
s.printer.Warning(s.message(message...))
}
func (s *PTermSpinner) Fail(message ...string) {
s.stopped = true
s.printer.Fail(s.message(message...))
}
func (s *PTermSpinner) Success(message ...string) {
s.stopped = true
s.printer.Success(s.message(message...))
}
func (s *PTermSpinner) Write(p []byte) (int, error) {
var lines []string
for _, line := range strings.Split(string(p), "\n") {
if line := strings.TrimSpace(line); len(line) > 0 {
lines = append(lines, line)
}
}
s.log = append(s.log, lines...)
// s.printer.UpdateText(s.message())
return len(p), nil
}
func (s *PTermSpinner) message(message ...string) string {
msg := s.prefix
if !s.start.IsZero() && s.stopped {
msg += " ⏱ " + time.Since(s.start).Round(0).String()
}
if value := strings.Join(message, " "); len(value) > 0 {
msg += "\n" + value
}
if pterm.PrintDebugMessages {
msg += "\n" + strings.Join(s.log, "\n")
}
return strings.TrimSpace(strings.ReplaceAll(msg, "\n", "\n "))
}

View File

@ -0,0 +1,20 @@
package util
import (
"github.com/pterm/pterm"
)
type PTermWriter struct {
printer pterm.PrefixPrinter
}
func NewPTermWriter(printer pterm.PrefixPrinter) *PTermWriter {
return &PTermWriter{
printer: printer,
}
}
func (p *PTermWriter) Write(b []byte) (int, error) {
p.printer.Println(string(b))
return len(b), nil
}

View File

@ -5,13 +5,11 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path"
"slices"
"strings"
"sync"
"time"
"github.com/foomo/squadron/internal/config"
"github.com/foomo/squadron/internal/jsonschema"
@ -38,6 +36,13 @@ type Squadron struct {
c config.Config
}
type statusDescription struct {
ManagedBy string `json:"managedBy,omitempty"`
DeployedBy string `json:"deployedBy,omitempty"`
GitCommit string `json:"gitCommit,omitempty"`
GitBranch string `json:"gitBranch,omitempty"`
}
func New(basePath, namespace string, files []string) *Squadron {
return &Squadron{
basePath: basePath,
@ -71,8 +76,7 @@ func (sq *Squadron) ConfigYAML() string {
// ------------------------------------------------------------------------------------------------
func (sq *Squadron) MergeConfigFiles(ctx context.Context) error {
pterm.Debug.Println("merging config files")
pterm.Debug.Println(strings.Join(append([]string{"using files"}, sq.files...), "\n└ "))
pterm.Debug.Println(strings.Join(append([]string{"merging config files"}, sq.files...), "\n└ "))
mergedFiles, err := conflate.FromFiles(sq.files...)
if err != nil {
@ -83,10 +87,12 @@ func (sq *Squadron) MergeConfigFiles(ctx context.Context) error {
return errors.Wrap(err, "failed to marshal yaml")
}
if err := yaml.Unmarshal(fileBytes, &sq.c); err != nil {
pterm.Error.Println(string(fileBytes))
return err
}
if sq.c.Version != config.Version {
return errors.New("Please upgrade your YAML definition to: " + config.Version)
pterm.Debug.Println(string(fileBytes))
return errors.New("Please upgrade your YAML definition to from '" + sq.c.Version + "' to '" + config.Version + "'")
}
sq.c.Trim(ctx)
@ -146,12 +152,10 @@ func (sq *Squadron) FilterConfig(ctx context.Context, squadron string, units, ta
}
func (sq *Squadron) RenderConfig(ctx context.Context) error {
pterm.Debug.Println("rendering config")
var tv templatex.Vars
var vars map[string]any
if err := yaml.Unmarshal([]byte(sq.config), &vars); err != nil {
return err
return errors.Wrap(err, "failed to render config")
}
// execute again with loaded template vars
tv = templatex.Vars{}
@ -168,23 +172,18 @@ func (sq *Squadron) RenderConfig(ctx context.Context) error {
tv.Add("Squadron", value)
}
// execute without errors to get existing values
pterm.Debug.Println("executing file template")
// pterm.Debug.Println(sq.config)
out1, 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")
out2, err := templatex.ExecuteFileTemplate(ctx, string(out1), tv, false)
if err != nil {
fmt.Print(util.Highlight(string(out1)))
return errors.Wrap(err, "failed to re-execute initial file template")
}
pterm.Debug.Println("unmarshalling vars")
if err := yaml.Unmarshal(out2, &vars); err != nil {
fmt.Print(util.Highlight(string(out2)))
return errors.Wrap(err, "failed to unmarshal vars")
@ -205,14 +204,12 @@ func (sq *Squadron) RenderConfig(ctx context.Context) error {
tv.Add("Squadron", value)
}
pterm.Debug.Println("executing file template")
out3, err := templatex.ExecuteFileTemplate(ctx, sq.config, tv, true)
if err != nil {
fmt.Print(util.Highlight(sq.config))
return errors.Wrap(err, "failed to execute second file template")
}
pterm.Debug.Println("unmarshalling vars")
if err := yaml.Unmarshal(out3, &sq.c); err != nil {
fmt.Print(util.Highlight(string(out3)))
return errors.Wrap(err, "failed to unmarshal vars")
@ -227,30 +224,52 @@ func (sq *Squadron) Push(ctx context.Context, pushArgs []string, parallel int) e
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(parallel)
printer := util.MustNewPTermMultiPrinter()
defer printer.Stop()
type one struct {
spinner *util.PTermSpinner
squadron string
unit string
item config.Build
}
var all []one
_ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
for _, name := range v.BuildNames() {
build := v.Builds[name]
id := fmt.Sprintf("%s/%s.%s", key, k, name)
wg.Go(func() error {
if err := ctx.Err(); err != nil {
return err
}
start := time.Now()
pterm.Info.Printfln("Push | %s\n└ %s:%s", id, build.Image, build.Tag)
if out, err := build.PushImage(ctx, key, k, pushArgs); err != nil {
pterm.Error.Printfln("Push | %s ⏱ %s\n└ %s:%s", id, time.Since(start).Round(time.Millisecond), build.Image, build.Tag)
return errors.Wrap(err, out)
}
pterm.Success.Printfln("Push | %s ⏱ %s\n└ %s:%s", id, time.Since(start).Round(time.Millisecond), build.Image, build.Tag)
return nil
spinner := printer.NewSpinner(fmt.Sprintf("🚚 | %s/%s.%s (%s:%s)", key, k, name, build.Image, build.Tag))
all = append(all, one{
spinner: spinner,
squadron: key,
unit: k,
item: build,
})
spinner.Start()
}
return nil
})
})
for _, a := range all {
wg.Go(func() error {
ctx := a.spinner.Inject(ctx)
if err := ctx.Err(); err != nil {
a.spinner.Warning(err.Error())
return err
}
if out, err := a.item.PushImage(ctx, a.squadron, a.unit, pushArgs); err != nil {
a.spinner.Fail(out)
return err
}
a.spinner.Success()
return nil
})
}
return wg.Wait()
}
@ -258,20 +277,27 @@ func (sq *Squadron) BuildDependencies(ctx context.Context, buildArgs []string, p
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(parallel)
printer := util.MustNewPTermMultiPrinter()
defer printer.Stop()
dependencies := sq.c.BuildDependencies(ctx)
for name, build := range dependencies {
wg.Go(func() error {
spinner := printer.NewSpinner(fmt.Sprintf("📦 | %s (%s:%s)", name, build.Image, build.Tag))
spinner.Start()
ctx := spinner.Inject(ctx)
if err := ctx.Err(); err != nil {
spinner.Warning(err.Error())
return err
}
start := time.Now()
pterm.Info.Printfln("Build | %s\n└ %s:%s", name, build.Image, build.Tag)
if out, err := build.Build(ctx, "", "", buildArgs); err != nil {
pterm.Error.Printfln("Build | %s ⏱ %s\n└ %s:%s", name, time.Since(start).Round(time.Millisecond), build.Image, build.Tag)
return errors.Wrap(err, out)
spinner.Fail(out)
return err
}
pterm.Success.Printfln("Build | %s ⏱ %s\n└ %s:%s", name, time.Since(start).Round(time.Millisecond), build.Image, build.Tag)
spinner.Success()
return nil
})
}
@ -287,30 +313,52 @@ func (sq *Squadron) Build(ctx context.Context, buildArgs []string, parallel int)
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(parallel)
printer := util.MustNewPTermMultiPrinter()
defer printer.Stop()
type one struct {
spinner *util.PTermSpinner
squadron string
unit string
item config.Build
}
var all []one
_ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
for _, name := range v.BuildNames() {
build := v.Builds[name]
id := fmt.Sprintf("%s/%s.%s", key, k, name)
wg.Go(func() error {
if err := ctx.Err(); err != nil {
return err
}
start := time.Now()
pterm.Info.Printfln("Build | %s\n└ %s:%s", id, build.Image, build.Tag)
if out, err := build.Build(ctx, key, k, buildArgs); err != nil {
pterm.Error.Printfln("Build | %s ⏱ %s\n└ %s:%s", id, time.Since(start).Round(time.Millisecond), build.Image, build.Tag)
return errors.Wrap(err, out)
}
pterm.Success.Printfln("Build | %s ⏱ %s\n└ %s:%s", id, time.Since(start).Round(time.Millisecond), build.Image, build.Tag)
return nil
spinner := printer.NewSpinner(fmt.Sprintf("📦 | %s/%s.%s (%s:%s)", key, k, name, build.Image, build.Tag))
all = append(all, one{
spinner: spinner,
squadron: key,
unit: k,
item: build,
})
spinner.Start()
}
return nil
})
})
for _, a := range all {
wg.Go(func() error {
ctx := a.spinner.Inject(ctx)
if err := ctx.Err(); err != nil {
a.spinner.Warning(err.Error())
return err
}
if out, err := a.item.Build(ctx, a.squadron, a.unit, buildArgs); err != nil {
a.spinner.Fail(out)
return err
}
a.spinner.Success()
return nil
})
}
return wg.Wait()
}
@ -318,33 +366,37 @@ func (sq *Squadron) Down(ctx context.Context, helmArgs []string, parallel int) e
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(parallel)
printer := util.MustNewPTermMultiPrinter()
defer printer.Stop()
_ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
wg.Go(func() error {
spinner := printer.NewSpinner(fmt.Sprintf("🗑️ | %s/%s", key, k))
spinner.Start()
ctx := spinner.Inject(ctx)
if err := ctx.Err(); err != nil {
spinner.Warning(err.Error())
return err
}
start := time.Now()
name := fmt.Sprintf("%s-%s", key, k)
namespace, err := sq.Namespace(ctx, key, k)
if err != nil {
return err
}
stdErr := bytes.NewBuffer([]byte{})
pterm.Info.Printfln("Down | %s/%s", key, k)
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) {
pterm.Error.Printfln("Down | %s/%s ⏱ %s", key, k, time.Since(start).Round(time.Millisecond))
return errors.Wrap(err, out)
strings.TrimSpace(out) != fmt.Sprintf("Error: uninstall: Release not loaded: %s: release: not found", name) {
spinner.Fail(out)
return err
}
pterm.Success.Printfln("Down | %s/%s ⏱ %s", key, k, time.Since(start).Round(time.Millisecond))
spinner.Success()
return nil
})
return nil
@ -360,7 +412,6 @@ func (sq *Squadron) RenderSchema(ctx context.Context, baseSchema string) (string
return "", errors.Wrap(err, "failed to load base schema")
}
pterm.Debug.Println("rendering schema")
if err := sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
if err := ctx.Err(); err != nil {
@ -374,22 +425,37 @@ func (sq *Squadron) RenderSchema(ctx context.Context, baseSchema string) (string
return js.SetSquadronUnitSchema(ctx, key, k, v.Chart.Schema)
})
}); err != nil {
return "", errors.Wrap(err, "failed to render schema")
return "", err
}
return js.PrettyString()
}
func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) error {
func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) (string, error) {
var m sync.Mutex
var ret bytes.Buffer
write := func(b []byte) error {
m.Lock()
defer m.Unlock()
_, err := ret.Write(b)
return err
}
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(parallel)
printer := util.MustNewPTermMultiPrinter()
defer printer.Stop()
_ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
wg.Go(func() error {
spinner := printer.NewSpinner(fmt.Sprintf("🔍 | %s/%s", key, k))
spinner.Start()
ctx := spinner.Inject(ctx)
if err := ctx.Err(); err != nil {
spinner.Warning(err.Error())
return err
}
@ -403,10 +469,10 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) e
return err
}
pterm.Debug.Printfln("running helm diff for: %s", k)
manifest, err := exec.CommandContext(ctx, "helm", "get", "manifest", name, "--namespace", namespace).CombinedOutput()
if err != nil && string(bytes.TrimSpace(manifest)) != errHelmReleaseNotFound {
return errors.Wrap(err, string(manifest))
spinner.Fail(string(manifest))
return err
}
cmd := exec.CommandContext(ctx, "helm", "upgrade", name,
"--install",
@ -434,19 +500,20 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) e
cmd.Args = append(cmd.Args, helmArgs...)
out, err := cmd.CombinedOutput()
if err != nil {
return errors.Wrap(err, string(out))
spinner.Fail(string(out))
return err
}
yamls1, err := yamldiff.Load(string(manifest))
if err != nil {
fmt.Print(util.Highlight(string(manifest)))
spinner.Fail(string(manifest))
return errors.Wrap(err, "failed to load yaml diff")
}
outStr := strings.Split(string(out), "\n")
yamls2, err := yamldiff.Load(strings.Join(outStr[10:], "\n"))
if err != nil {
fmt.Print(util.Highlight(string(out)))
spinner.Fail(string(out))
return errors.Wrap(err, "failed to load yaml diff")
}
@ -455,25 +522,24 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) e
res += diff.Dump() + " ---\n"
}
m.Lock()
defer m.Unlock()
pterm.Info.Printfln("Diff | %s/%s", key, k)
fmt.Print(util.Highlight(res))
if err := write([]byte(res)); err != nil {
spinner.Fail(res)
return err
}
spinner.Success()
return nil
})
return nil
})
})
return wg.Wait()
}
if err := wg.Wait(); err != nil {
return "", err
}
type statusDescription struct {
ManagedBy string `json:"managedBy,omitempty"`
DeployedBy string `json:"deployedBy,omitempty"`
GitCommit string `json:"gitCommit,omitempty"`
GitBranch string `json:"gitBranch,omitempty"`
return ret.String(), nil
}
func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int) error {
@ -506,27 +572,46 @@ func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int)
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(parallel)
printer := util.MustNewPTermMultiPrinter()
defer printer.Stop()
_ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
return value.Iterate(ctx, func(ctx context.Context, 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
return errors.Errorf("failed to retrieve namsspace: %s/%s", key, k)
}
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 {
wg.Go(func() error {
spinner := printer.NewSpinner(fmt.Sprintf("📄 | %s/%s", key, k))
spinner.Start()
ctx := spinner.Inject(ctx)
if err := ctx.Err(); err != nil {
spinner.Warning(err.Error())
return err
}
stdErr := bytes.NewBuffer([]byte{})
out, err := util.NewHelmCommand().Args("status", name).
Stderr(stdErr).
Args("--namespace", namespace, "--output", "json", "--show-desc").
Args(helmArgs...).Run(ctx)
if err != nil && string(bytes.TrimSpace(stdErr.Bytes())) == errHelmReleaseNotFound {
tbd = append(tbd, []string{name, "0", "not installed", "", ""})
} else if err != nil {
spinner.Fail(stdErr.String())
return err
}
if err := json.Unmarshal([]byte(out), &status); err != nil {
spinner.Fail(out)
return errors.Errorf("failed to retrieve status: %s/%s", key, k)
}
var notes []string
lines := strings.Split(status.Info.Description, "\n")
var statusDescription statusDescription
@ -565,7 +650,11 @@ func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int)
status.Info.LastDeployed,
strings.Join(notes, "\n"),
})
}
spinner.Success()
return nil
})
return nil
})
})
@ -574,7 +663,13 @@ func (sq *Squadron) Status(ctx context.Context, helmArgs []string, parallel int)
return err
}
return pterm.DefaultTable.WithHasHeader().WithData(tbd).Render()
out, err := pterm.DefaultTable.WithHasHeader().WithData(tbd).Srender()
if err != nil {
return err
}
pterm.Println(out)
return nil
}
func (sq *Squadron) Rollback(ctx context.Context, revision string, helmArgs []string, parallel int) error {
@ -585,6 +680,9 @@ func (sq *Squadron) Rollback(ctx context.Context, revision string, helmArgs []st
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(parallel)
printer := util.MustNewPTermMultiPrinter()
defer printer.Stop()
_ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
name := fmt.Sprintf("%s-%s", key, k)
@ -593,17 +691,32 @@ func (sq *Squadron) Rollback(ctx context.Context, revision string, helmArgs []st
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)
}
wg.Go(func() error {
spinner := printer.NewSpinner(fmt.Sprintf("♻️ | %s/%s", key, k))
spinner.Start()
ctx := spinner.Inject(ctx)
if err := ctx.Err(); err != nil {
spinner.Warning(err.Error())
return err
}
stdErr := bytes.NewBuffer([]byte{})
out, err := util.NewHelmCommand().Args("rollback", name).
Stderr(stdErr).
// Stdout(os.Stdout).
Args(helmArgs...).
Args("--namespace", namespace).
Run(ctx)
if err != nil &&
string(bytes.TrimSpace(stdErr.Bytes())) != fmt.Sprintf("Error: uninstall: Release not loaded: %s: release: not found", name) {
spinner.Fail(stdErr.String())
return err
}
spinner.Success(out)
return nil
})
return nil
})
@ -664,61 +777,89 @@ func (sq *Squadron) Up(ctx context.Context, helmArgs []string, username, version
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(parallel)
printer := util.MustNewPTermMultiPrinter()
defer printer.Stop()
type one struct {
spinner *util.PTermSpinner
squadron string
unit string
item *config.Unit
}
var all []one
_ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
wg.Go(func() error {
if err := ctx.Err(); err != nil {
return err
}
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
}
// 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", "global.foomo.squadron.name="+key).
Args("--set", "global.foomo.squadron.unit="+k).
Args("--description", string(description)).
Args("--namespace", namespace).
Args("--dependency-update").
Args(v.PostRendererArgs()...).
Args("--install").
Args("--values", "-").
Args(helmArgs...)
if strings.HasPrefix(v.Chart.Repository, "file://") {
cmd.Args(path.Clean(strings.TrimPrefix(v.Chart.Repository, "file://")))
} else {
cmd.Args(v.Chart.Name)
if v.Chart.Repository != "" {
cmd.Args("--repo", v.Chart.Repository)
}
if v.Chart.Version != "" {
cmd.Args("--version", v.Chart.Version)
}
}
if out, err := cmd.Run(ctx); err != nil {
return errors.Wrap(err, out)
}
return nil
spinner := printer.NewSpinner(fmt.Sprintf("🚀 | %s/%s", key, k))
all = append(all, one{
spinner: spinner,
squadron: key,
unit: k,
item: v,
})
spinner.Start()
return nil
})
})
for _, a := range all {
wg.Go(func() error {
ctx := a.spinner.Inject(ctx)
if err := ctx.Err(); err != nil {
a.spinner.Warning(err.Error())
return err
}
name := fmt.Sprintf("%s-%s", a.squadron, a.unit)
namespace, err := sq.Namespace(ctx, a.squadron, a.unit)
if err != nil {
a.spinner.Fail(err.Error())
return err
}
valueBytes, err := a.item.ValuesYAML(sq.c.Global)
if err != nil {
a.spinner.Fail(err.Error())
return err
}
// install chart
cmd := util.NewHelmCommand().
Stdin(bytes.NewReader(valueBytes)).
// Stdout(os.Stdout).
Args("upgrade", name, "--install").
Args("--set", "global.foomo.squadron.name="+a.squadron).
Args("--set", "global.foomo.squadron.unit="+a.unit).
Args("--description", string(description)).
Args("--namespace", namespace).
Args("--dependency-update").
Args(a.item.PostRendererArgs()...).
Args("--install").
Args("--values", "-").
Args(helmArgs...)
if strings.HasPrefix(a.item.Chart.Repository, "file://") {
cmd.Args(path.Clean(strings.TrimPrefix(a.item.Chart.Repository, "file://")))
} else {
cmd.Args(a.item.Chart.Name)
if a.item.Chart.Repository != "" {
cmd.Args("--repo", a.item.Chart.Repository)
}
if a.item.Chart.Version != "" {
cmd.Args("--version", a.item.Chart.Version)
}
}
out, err := cmd.Run(ctx)
if err != nil {
a.spinner.Fail(out)
return err
}
a.spinner.Success()
return nil
})
}
return wg.Wait()
}
@ -735,26 +876,41 @@ func (sq *Squadron) Template(ctx context.Context, helmArgs []string, parallel in
wg, ctx := errgroup.WithContext(ctx)
wg.SetLimit(parallel)
printer := util.MustNewPTermMultiPrinter()
defer printer.Stop()
_ = sq.Config().Squadrons.Iterate(ctx, func(ctx context.Context, key string, value config.Map[*config.Unit]) error {
return value.Iterate(ctx, func(ctx context.Context, k string, v *config.Unit) error {
wg.Go(func() error {
spinner := printer.NewSpinner(fmt.Sprintf("🧾 | %s/%s", key, k))
spinner.Start()
ctx := spinner.Inject(ctx)
if err := ctx.Err(); err != nil {
spinner.Warning(err.Error())
return err
}
name := fmt.Sprintf("%s-%s", key, k)
namespace, err := sq.Namespace(ctx, key, k)
if err != nil {
return err
spinner.Fail(err.Error())
return errors.Errorf("failed to retrieve namsspace: %s/%s", key, k)
}
pterm.Debug.Printfln("running helm template for chart: %s", name)
out, err := v.Template(ctx, name, key, k, namespace, sq.c.Global, helmArgs)
if err != nil {
spinner.Fail(string(out))
return err
}
return write(out)
if err := write(out); err != nil {
spinner.Fail(string(out))
return err
}
spinner.Success()
return nil
})
return nil

View File

@ -1,7 +1,6 @@
package squadron_test
import (
"context"
"encoding/json"
"errors"
"os"
@ -102,7 +101,7 @@ func TestConfigSimpleSnapshot(t *testing.T) {
func runTestConfig(t *testing.T, name string, files []string, squadronName string, unitNames, tags []string) {
t.Helper()
var cwd string
ctx := context.TODO()
ctx := t.Context()
require.NoError(t, util.ValidatePath(".", &cwd))
for i, file := range files {