feat: support squadron schema v2

This commit is contained in:
Kevin Franklin Kim 2024-01-04 08:33:26 +01:00
parent f3c9d0413a
commit ccd20cafad
No known key found for this signature in database
11 changed files with 902 additions and 173 deletions

View File

@ -18,7 +18,7 @@ type (
Command struct {
l log.Logger
kubectl *kubectl.Kubectl
squadron *squadron.Squadron
squadron squadron.Squadron
commandTree tree.Root
namespaceFn NamespaceFn
}
@ -40,7 +40,7 @@ func CommandWithNamespaceFn(v NamespaceFn) CommandOption {
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewCommand(l log.Logger, kubectl *kubectl.Kubectl, squadron *squadron.Squadron, opts ...CommandOption) *Command {
func NewCommand(l log.Logger, kubectl *kubectl.Kubectl, squadron squadron.Squadron, opts ...CommandOption) *Command {
inst := &Command{
l: l.Named("k9s"),
kubectl: kubectl,

View File

@ -0,0 +1,6 @@
package squadron
type Cluster struct {
Name string `json:"name" yaml:"name"`
Fleets []string `json:"fleets" yaml:"fleets"`
}

View File

@ -21,8 +21,3 @@ func (c Config) ClusterNames() []string {
}
return ret
}
type Cluster struct {
Name string `json:"name" yaml:"name"`
Fleets []string `json:"fleets" yaml:"fleets"`
}

View File

@ -2,170 +2,14 @@ package squadron
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/foomo/posh-providers/kubernets/kubectl"
"github.com/foomo/posh/pkg/log"
"github.com/foomo/posh/pkg/shell"
"github.com/pkg/errors"
"github.com/spf13/viper"
"golang.org/x/exp/slices"
)
type (
Squadron struct {
l log.Logger
cfg Config
configKey string
kubectl *kubectl.Kubectl
}
Option func(*Squadron) error
)
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func WithConfigKey(v string) Option {
return func(o *Squadron) error {
o.configKey = v
return nil
}
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func New(l log.Logger, kubectl *kubectl.Kubectl, opts ...Option) (*Squadron, error) {
inst := &Squadron{
l: l,
kubectl: kubectl,
configKey: "squadron",
}
for _, opt := range opts {
if opt != nil {
if err := opt(inst); err != nil {
return nil, err
}
}
}
if err := viper.UnmarshalKey(inst.configKey, &inst.cfg); err != nil {
return nil, err
}
return inst, nil
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (s *Squadron) Cluster(name string) (Cluster, bool) {
return s.cfg.Cluster(name)
}
func (s *Squadron) Exists(name string) bool {
if _, err := os.Stat(filepath.Join(s.cfg.Path, name, "squadron.yaml")); err != nil {
return false
}
return true
}
func (s *Squadron) UnitExists(ctx context.Context, squadron, cluster, fleet, name string, override bool) bool {
units, _ := s.ListUnits(ctx, squadron, cluster, fleet, override)
return slices.Contains(units, name)
}
func (s *Squadron) List() ([]string, error) {
files, err := os.ReadDir(s.cfg.Path)
if err != nil {
return nil, err
}
var results []string
for _, value := range files {
if value.IsDir() && !strings.HasPrefix(value.Name(), ".") {
results = append(results, value.Name())
}
}
return results, nil
}
func (s *Squadron) ListUnits(ctx context.Context, squadron, cluster, fleet string, override bool) ([]string, error) {
var units []string
files := strings.Join(s.GetFiles(squadron, cluster, fleet, override), ",")
out, err := shell.New(ctx, s.l, "squadron", "list", "-f", files).
Dir(path.Join(s.cfg.Path, squadron)).
Output()
if err != nil {
return nil, errors.WithMessage(err, string(out))
}
for _, line := range strings.Split(string(out), "\n") {
if line == "" {
continue
}
units = append(units, line)
}
return units, nil
}
func (s *Squadron) UnitDirs(squadron string) []string {
var units []string
dirs, err := os.ReadDir(filepath.Join(s.cfg.Path, squadron))
if err != nil {
return nil
}
for _, dir := range dirs {
if _, err := os.Stat(filepath.Join(s.cfg.Path, squadron, dir.Name(), "squadron.yaml")); err == nil {
units = append(units, dir.Name())
}
}
return units
}
func (s *Squadron) GetFiles(squadron, cluster, fleet string, override bool) []string {
var files []string
allFiles := []string{
"../squadron.yaml",
"../squadron.override.yaml",
fmt.Sprintf("../squadron.%s.yaml", fleet),
fmt.Sprintf("../squadron.%s.override.yaml", fleet),
fmt.Sprintf("../squadron.%s.yaml", cluster),
fmt.Sprintf("../squadron.%s.override.yaml", cluster),
fmt.Sprintf("../squadron.%s.%s.yaml", cluster, fleet),
fmt.Sprintf("../squadron.%s.%s.override.yaml", cluster, fleet),
"squadron.yaml",
"squadron.override.yaml",
fmt.Sprintf("squadron.%s.yaml", fleet),
fmt.Sprintf("squadron.%s.override.yaml", fleet),
fmt.Sprintf("squadron.%s.yaml", cluster),
fmt.Sprintf("squadron.%s.override.yaml", cluster),
fmt.Sprintf("squadron.%s.%s.yaml", cluster, fleet),
fmt.Sprintf("squadron.%s.%s.override.yaml", cluster, fleet),
}
for _, unit := range s.UnitDirs(squadron) {
allFiles = append(allFiles,
fmt.Sprintf("%s/squadron.yaml", unit),
fmt.Sprintf("%s/squadron.override.yaml", unit),
fmt.Sprintf("%s/squadron.%s.yaml", unit, fleet),
fmt.Sprintf("%s/squadron.%s.override.yaml", unit, fleet),
fmt.Sprintf("%s/squadron.%s.yaml", unit, cluster),
fmt.Sprintf("%s/squadron.%s.override.yaml", unit, cluster),
fmt.Sprintf("%s/squadron.%s.%s.yaml", unit, cluster, fleet),
fmt.Sprintf("%s/squadron.%s.%s.override.yaml", unit, cluster, fleet),
)
}
for _, file := range allFiles {
if !override && strings.Contains(file, ".override.") {
continue
}
if _, err := os.Stat(filepath.Join(s.cfg.Path, squadron, file)); err == nil {
files = append(files, file)
}
}
return files
type Squadron interface {
Cluster(name string) (Cluster, bool)
Exists(name string) bool
UnitExists(ctx context.Context, squadron, cluster, fleet, name string, override bool) bool
List() ([]string, error)
ListUnits(ctx context.Context, squadron, cluster, fleet string, override bool) ([]string, error)
UnitDirs(squadron string) []string
GetFiles(squadron, cluster, fleet string, override bool) []string
}

View File

@ -0,0 +1,172 @@
package squadron
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/foomo/posh-providers/foomo/squadron"
"github.com/foomo/posh-providers/kubernets/kubectl"
"github.com/foomo/posh/pkg/log"
"github.com/foomo/posh/pkg/shell"
"github.com/pkg/errors"
"github.com/spf13/viper"
"golang.org/x/exp/slices"
)
type (
Squadron struct {
l log.Logger
cfg squadron.Config
configKey string
kubectl *kubectl.Kubectl
}
Option func(*Squadron) error
)
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func WithConfigKey(v string) Option {
return func(o *Squadron) error {
o.configKey = v
return nil
}
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func New(l log.Logger, kubectl *kubectl.Kubectl, opts ...Option) (*Squadron, error) {
inst := &Squadron{
l: l,
kubectl: kubectl,
configKey: "squadron",
}
for _, opt := range opts {
if opt != nil {
if err := opt(inst); err != nil {
return nil, err
}
}
}
if err := viper.UnmarshalKey(inst.configKey, &inst.cfg); err != nil {
return nil, err
}
return inst, nil
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (s *Squadron) Cluster(name string) (squadron.Cluster, bool) {
return s.cfg.Cluster(name)
}
func (s *Squadron) Exists(name string) bool {
if _, err := os.Stat(filepath.Join(s.cfg.Path, name, "squadron.yaml")); err != nil {
return false
}
return true
}
func (s *Squadron) UnitExists(ctx context.Context, squadron, cluster, fleet, name string, override bool) bool {
units, _ := s.ListUnits(ctx, squadron, cluster, fleet, override)
return slices.Contains(units, name)
}
func (s *Squadron) List() ([]string, error) {
files, err := os.ReadDir(s.cfg.Path)
if err != nil {
return nil, err
}
var results []string
for _, value := range files {
if value.IsDir() && !strings.HasPrefix(value.Name(), ".") {
results = append(results, value.Name())
}
}
return results, nil
}
func (s *Squadron) ListUnits(ctx context.Context, squadron, cluster, fleet string, override bool) ([]string, error) {
var units []string
files := strings.Join(s.GetFiles(squadron, cluster, fleet, override), ",")
out, err := shell.New(ctx, s.l, "squadron", "list", "-f", files).
Dir(path.Join(s.cfg.Path, squadron)).
Output()
if err != nil {
return nil, errors.WithMessage(err, string(out))
}
for _, line := range strings.Split(string(out), "\n") {
if line == "" {
continue
}
units = append(units, line)
}
return units, nil
}
func (s *Squadron) UnitDirs(squadron string) []string {
var units []string
dirs, err := os.ReadDir(filepath.Join(s.cfg.Path, squadron))
if err != nil {
return nil
}
for _, dir := range dirs {
if _, err := os.Stat(filepath.Join(s.cfg.Path, squadron, dir.Name(), "squadron.yaml")); err == nil {
units = append(units, dir.Name())
}
}
return units
}
func (s *Squadron) GetFiles(squadron, cluster, fleet string, override bool) []string {
var files []string
allFiles := []string{
"../squadron.yaml",
"../squadron.override.yaml",
fmt.Sprintf("../squadron.%s.yaml", fleet),
fmt.Sprintf("../squadron.%s.override.yaml", fleet),
fmt.Sprintf("../squadron.%s.yaml", cluster),
fmt.Sprintf("../squadron.%s.override.yaml", cluster),
fmt.Sprintf("../squadron.%s.%s.yaml", cluster, fleet),
fmt.Sprintf("../squadron.%s.%s.override.yaml", cluster, fleet),
"squadron.yaml",
"squadron.override.yaml",
fmt.Sprintf("squadron.%s.yaml", fleet),
fmt.Sprintf("squadron.%s.override.yaml", fleet),
fmt.Sprintf("squadron.%s.yaml", cluster),
fmt.Sprintf("squadron.%s.override.yaml", cluster),
fmt.Sprintf("squadron.%s.%s.yaml", cluster, fleet),
fmt.Sprintf("squadron.%s.%s.override.yaml", cluster, fleet),
}
for _, unit := range s.UnitDirs(squadron) {
allFiles = append(allFiles,
fmt.Sprintf("%s/squadron.yaml", unit),
fmt.Sprintf("%s/squadron.override.yaml", unit),
fmt.Sprintf("%s/squadron.%s.yaml", unit, fleet),
fmt.Sprintf("%s/squadron.%s.override.yaml", unit, fleet),
fmt.Sprintf("%s/squadron.%s.yaml", unit, cluster),
fmt.Sprintf("%s/squadron.%s.override.yaml", unit, cluster),
fmt.Sprintf("%s/squadron.%s.%s.yaml", unit, cluster, fleet),
fmt.Sprintf("%s/squadron.%s.%s.override.yaml", unit, cluster, fleet),
)
}
for _, file := range allFiles {
if !override && strings.Contains(file, ".override.") {
continue
}
if _, err := os.Stat(filepath.Join(s.cfg.Path, squadron, file)); err == nil {
files = append(files, file)
}
}
return files
}

View File

@ -0,0 +1,522 @@
package squadron
import (
"context"
"fmt"
"strings"
"github.com/foomo/posh-providers/kubernets/kubectl"
"github.com/foomo/posh-providers/onepassword"
"github.com/foomo/posh-providers/slack-go/slack"
"github.com/foomo/posh/pkg/cache"
"github.com/foomo/posh/pkg/command/tree"
"github.com/foomo/posh/pkg/log"
"github.com/foomo/posh/pkg/prompt/goprompt"
"github.com/foomo/posh/pkg/readline"
"github.com/foomo/posh/pkg/shell"
"github.com/foomo/posh/pkg/util/git"
"github.com/foomo/posh/pkg/util/suggests"
"github.com/pkg/errors"
slackgo "github.com/slack-go/slack"
"golang.org/x/exp/slices"
)
const All = "all"
type (
Command struct {
l log.Logger
op *onepassword.OnePassword
slack *slack.Slack
slackChannelID string
cache cache.Namespace
kubectl *kubectl.Kubectl
squadron *Squadron
commandTree tree.Root
namespaceFn NamespaceFn
}
NamespaceFn func(cluster, fleet string) string
CommandOption func(*Command)
)
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func CommandWithNamespaceFn(v NamespaceFn) CommandOption {
return func(o *Command) {
o.namespaceFn = v
}
}
func CommandWithSlack(v *slack.Slack) CommandOption {
return func(o *Command) {
o.slack = v
}
}
func CommandWithSlackChannelID(v string) CommandOption {
return func(o *Command) {
o.slackChannelID = v
}
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewCommand(l log.Logger, squadron *Squadron, kubectl *kubectl.Kubectl, op *onepassword.OnePassword, cache cache.Cache, opts ...CommandOption) *Command {
inst := &Command{
l: l.Named("squadron"),
op: op,
cache: cache.Get("squadron"),
kubectl: kubectl,
squadron: squadron,
slackChannelID: "squadron",
namespaceFn: func(cluster, fleet string) string {
if fleet == "default" {
return "{{ .Squadron }}"
} else {
return fmt.Sprintf("%s-{{ .Squadron }}", fleet)
}
},
}
for _, opt := range opts {
if opt != nil {
opt(inst)
}
}
unitsArg := &tree.Arg{
Name: "unit",
Repeat: true,
Optional: true,
Suggest: func(ctx context.Context, t tree.Root, r *readline.Readline) []goprompt.Suggest {
cluster := r.Args().At(0)
fleet := r.Args().At(1)
squadron := r.Args().At(2)
return inst.cache.Get(fmt.Sprintf("units-%s-%s-%s", squadron, cluster, fleet), func() any {
if value, err := inst.squadron.ListUnits(ctx, squadron, cluster, fleet, true); err != nil {
return []goprompt.Suggest{}
} else {
return suggests.List(value)
}
}).([]goprompt.Suggest)
},
}
slackFlag := func(fs *readline.FlagSets) {
if inst.slack != nil {
fs.Internal().Bool("slack", false, "send slack notification")
}
}
profileFlag := func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
if r.Args().HasIndex(0) {
fs.Internal().String("profile", "", "Profile to use.")
if err := fs.Internal().SetValues("profile", inst.kubectl.Cluster(r.Args().At(0)).Profiles(ctx)...); err != nil {
return err
}
}
return nil
}
commonFlags := func(fs *readline.FlagSets) {
fs.Internal().Bool("no-override", false, "ignore override files")
fs.Default().Bool("verbose", inst.l.IsLevel(log.LevelDebug), "set verbose level")
fs.Default().Bool("debug", inst.l.IsLevel(log.LevelTrace), "set debug level")
}
clusterValues := func(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
var ret []string
for _, cluster := range inst.kubectl.Clusters() {
if _, ok := inst.squadron.cfg.Cluster(cluster.Name()); ok {
ret = append(ret, cluster.Name())
}
}
return suggests.List(ret)
}
inst.commandTree = tree.New(&tree.Node{
Name: "squadron",
Description: "Manage your squadron",
Nodes: tree.Nodes{
{
Name: "cluster",
Values: clusterValues,
Nodes: tree.Nodes{
{
Name: "fleet",
Values: func(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
var ret []string
if cluster, ok := inst.squadron.cfg.Cluster(r.Args().At(0)); ok {
ret = cluster.Fleets
}
return suggests.List(ret)
},
Nodes: tree.Nodes{
{
Name: "squadron",
Values: func(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
if value, err := inst.squadron.List(); err != nil {
inst.l.Debug(err.Error())
return nil
} else {
return suggests.List(append(value, All))
}
},
Nodes: tree.Nodes{
{
Name: "build",
Description: "Build or rebuild squadron units",
Args: tree.Args{unitsArg},
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
commonFlags(fs)
fs.Default().Bool("push", false, "push image")
fs.Default().Int64("parallel", 1, "number of parallel processes")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
// build args
fs.Get("build-args").StringArray("build-arg", nil, "set build-time variables")
fs.Internal().String("tag", "", "image tag")
return nil
},
Execute: inst.execute,
},
{
Name: "config",
Description: "View generated squadron config",
Args: tree.Args{unitsArg},
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
commonFlags(fs)
fs.Default().Bool("no-render", false, "don't render the config template")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
return nil
},
Execute: inst.execute,
},
{
Name: "diff",
Description: "Shows the diff between the installed and local chart",
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
commonFlags(fs)
fs.Default().Int64("parallel", 1, "number of parallel processes")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
return nil
},
Execute: inst.execute,
},
{
Name: "down",
Description: "Uninstalls the squadron chart",
Args: tree.Args{unitsArg},
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
slackFlag(fs)
commonFlags(fs)
fs.Default().Int64("parallel", 1, "number of parallel processes")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
if err := profileFlag(ctx, r, fs); err != nil {
return err
}
return nil
},
Execute: inst.execute,
},
{
Name: "list",
Description: "List squadron units",
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
commonFlags(fs)
fs.Default().Bool("charts", false, "include charts")
fs.Default().Bool("builds", false, "include builds")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
return nil
},
Execute: inst.execute,
},
{
Name: "push",
Description: "Push squadron units",
Args: tree.Args{unitsArg},
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
commonFlags(fs)
fs.Default().Bool("build", false, "build image")
fs.Default().Int64("parallel", 1, "number of parallel processes")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
// build args
fs.Get("build-args").StringArray("build-arg", nil, "set build-time variables")
fs.Internal().String("tag", "", "image tag")
return nil
},
Execute: inst.execute,
},
{
Name: "rollback",
Description: "Roll back the squadron chart",
Args: tree.Args{unitsArg},
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
slackFlag(fs)
commonFlags(fs)
fs.Default().Int64("parallel", 1, "number of parallel processes")
fs.Default().String("revision", "", "revision number to rollback to")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
if err := profileFlag(ctx, r, fs); err != nil {
return err
}
return nil
},
Execute: inst.execute,
},
{
Name: "status",
Description: "Display the status of the units",
Args: tree.Args{unitsArg},
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
commonFlags(fs)
fs.Default().Int64("parallel", 1, "number of parallel processes")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
if err := profileFlag(ctx, r, fs); err != nil {
return err
}
return nil
},
Execute: inst.execute,
},
{
Name: "template",
Description: "Render chart templates locally and display the output",
Args: tree.Args{unitsArg},
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
commonFlags(fs)
fs.Default().Int64("parallel", 1, "number of parallel processes")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
fs.Internal().String("tag", "", "image tag")
return nil
},
Execute: inst.execute,
},
{
Name: "up",
Description: "Installs a squadron chart",
Args: tree.Args{unitsArg},
Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error {
slackFlag(fs)
commonFlags(fs)
if err := profileFlag(ctx, r, fs); err != nil {
return err
}
fs.Default().Bool("push", false, "push image")
fs.Default().Bool("build", false, "build image")
fs.Default().Int64("parallel", 1, "number of parallel processes")
fs.Default().StringSlice("tags", nil, "list of tags to include or exclude")
// build args
fs.Get("build-args").StringArray("build-arg", nil, "set build-time variables")
// internal
fs.Internal().String("tag", "", "image tag")
fs.Internal().Bool("create-namespace", false, "create namespace if not exist")
return nil
},
Execute: inst.execute,
},
},
},
},
},
},
},
},
})
return inst
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (c *Command) Name() string {
return c.commandTree.Node().Name
}
func (c *Command) Description() string {
return c.commandTree.Node().Description
}
func (c *Command) Complete(ctx context.Context, r *readline.Readline) []goprompt.Suggest {
return c.commandTree.Complete(ctx, r)
}
func (c *Command) Execute(ctx context.Context, r *readline.Readline) error {
return c.commandTree.Execute(ctx, r)
}
func (c *Command) Help(ctx context.Context, r *readline.Readline) string {
return c.commandTree.Help(ctx, r)
}
// ------------------------------------------------------------------------------------------------
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (c *Command) execute(ctx context.Context, r *readline.Readline) error {
var env []string
var buildArgs []string
fs := r.FlagSets().Default()
ifs := r.FlagSets().Internal()
bfs := r.FlagSets().Get("build-args")
passFlags := []string{"--"}
cluster, fleet, squadron, cmd, units := r.Args()[0], r.Args()[1], r.Args()[2], r.Args()[3], r.Args()[4:]
// retrieve flags
tag, _ := ifs.GetString("tag")
noOverride := log.MustGet(ifs.GetBool("no-override"))(c.l)
// try retrieve profile
profile, _ := ifs.GetString("profile")
{ // handle 1password
if c.op != nil {
if ok, _ := c.op.IsAuthenticated(ctx); !ok {
c.l.Info("missing 1password session, please login")
if err := c.op.SignIn(ctx); err != nil {
return err
}
}
}
}
{ // handle pass through flags
if slices.Contains([]string{"up", "diff", "template"}, cmd) {
passFlags = append(passFlags, "--set", fmt.Sprintf("fleet=%v", fleet))
}
if slices.Contains([]string{"up"}, cmd) {
if log.MustGet(ifs.GetBool("create-namespace"))(c.l) {
passFlags = append(passFlags, "--create-namespace")
}
}
}
{ // handle env
env = append(env, fmt.Sprintf("FLEET=%s", fleet))
if tag != "" {
env = append(env, fmt.Sprintf("TAG=%q", tag))
}
env = append(env, c.kubectl.Cluster(cluster).Env(profile))
}
{ // handle slack
if ok, _ := ifs.GetBool("slack"); ok {
if err := c.notify(ctx, cmd, cluster, fleet, squadron, tag, units); err != nil {
return err
}
}
}
c.l.Infof("Fleet: %s", fleet)
c.l.Infof("Cluster: %s", cluster)
if value, err := bfs.GetStringArray("build-arg"); err == nil && len(value) > 0 {
for _, v := range value {
buildArgs = append(buildArgs, "--build-arg", v)
}
}
// flatten build args
if len(buildArgs) > 0 {
buildArgs = []string{fmt.Sprintf("--build-args='%s'", strings.Join(buildArgs, " "))}
}
sh := shell.New(ctx, c.l, "squadron", cmd).
Args("--file", strings.Join(c.squadron.GetFiles("", cluster, fleet, !noOverride), ",")).
Dir(c.squadron.cfg.Path).
Env(env...)
if squadron != All {
sh.Args(squadron).Args(units...)
}
flags := r.FlagSets().Default().Args()
if slices.Contains([]string{"up", "diff", "down", "rollback", "status", "template"}, cmd) {
flags = append(flags, "--namespace", c.namespaceFn(cluster, fleet))
}
if err := sh.
Args(flags...).
Args(fs.Visited().Args()...).
Args(buildArgs...).
Args(passFlags...).
Args(r.AdditionalArgs()...).
Run(); err != nil {
return errors.Wrap(err, "failed to execute squadron")
}
return nil
}
func (c *Command) notify(ctx context.Context, cmd, cluster, fleet, squadron, tag string, units []string) error {
if tag == "" {
tag = "latest"
}
user, err := git.ConfigUserName(ctx, c.l)
if err != nil {
c.l.Debug("failed to get git user:", err.Error())
user = "unknown"
}
ref, err := git.Ref(ctx, c.l)
if err != nil {
c.l.Debug("failed to get git ref:", err.Error())
ref = "unknown"
}
var msg *slackgo.SectionBlock
switch cmd {
case "up":
if squadron == All {
msg = c.slack.MarkdownSection(fmt.Sprintf("🚢 Full deployment to *%s* | *%s* _(%s)_", cluster, fleet, tag))
} else if len(units) == 0 {
msg = c.slack.MarkdownSection(fmt.Sprintf("🛥 Deployment to *%s*\n\n- %s.all | *%s* _(%s)_\n", cluster, squadron, fleet, tag))
} else {
str := make([]string, 0, len(units))
for _, unit := range units {
str = append(str, "- "+squadron+"."+unit)
}
msg = c.slack.MarkdownSection(fmt.Sprintf("🛶 Deployment to *%s* | *%s* _(%s)_\n\n%s\n", cluster, fleet, tag, strings.Join(str, "\n")))
}
case "down":
if squadron == All {
msg = c.slack.MarkdownSection(fmt.Sprintf("🪦 Full uninstallation of *%s* | *%s*", cluster, fleet))
} else if len(units) == 0 {
msg = c.slack.MarkdownSection(fmt.Sprintf("💀 Uninstalling from *%s*\n\n- %s.all | *%s*\n", cluster, squadron, fleet))
} else {
str := make([]string, 0, len(units))
for _, unit := range units {
str = append(str, "- "+squadron+"."+unit)
}
msg = c.slack.MarkdownSection(fmt.Sprintf("🗑 Uninstalling from *%s* | *%s*\n\n%s\n", cluster, fleet, strings.Join(str, "\n")))
}
case "rollback":
if squadron == "all" {
msg = c.slack.MarkdownSection(fmt.Sprintf("⏬ Full roll back of *%s* | *%s*", cluster, fleet))
} else if len(units) == 0 {
msg = c.slack.MarkdownSection(fmt.Sprintf("⏪ Rollback in *%s*\n\n- %s.all | *%s*\n", cluster, squadron, fleet))
} else {
str := make([]string, 0, len(units))
for _, unit := range units {
str = append(str, "- "+squadron+"."+unit)
}
msg = c.slack.MarkdownSection(fmt.Sprintf("🔙 Rollback in *%s* | *%s*\n\n%s\n", cluster, fleet, strings.Join(str, "\n")))
}
default:
c.l.Debug("skipping notification for cmd:", cmd)
}
blockOpt := slackgo.MsgOptionBlocks(
msg,
slackgo.NewContextBlock("", slackgo.NewTextBlockObject("mrkdwn", ref+" by "+user, false, false)),
c.slack.DividerSection(),
)
fallbackOpt := slackgo.MsgOptionText(fmt.Sprintf("Deployment to %s | %s", cluster, fleet), false)
return c.slack.Send(
ctx,
c.slack.Channel(c.slackChannelID),
slackgo.MsgOptionCompose(fallbackOpt, blockOpt),
)
}

View File

@ -0,0 +1,187 @@
package squadron
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/acarl005/stripansi"
"github.com/foomo/posh-providers/foomo/squadron"
"github.com/foomo/posh-providers/kubernets/kubectl"
"github.com/foomo/posh/pkg/log"
"github.com/foomo/posh/pkg/shell"
"github.com/pkg/errors"
"github.com/spf13/viper"
"golang.org/x/exp/slices"
)
type (
Squadron struct {
l log.Logger
cfg squadron.Config
configKey string
kubectl *kubectl.Kubectl
}
Option func(*Squadron) error
)
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func WithConfigKey(v string) Option {
return func(o *Squadron) error {
o.configKey = v
return nil
}
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func New(l log.Logger, kubectl *kubectl.Kubectl, opts ...Option) (*Squadron, error) {
inst := &Squadron{
l: l,
kubectl: kubectl,
configKey: "squadron",
}
for _, opt := range opts {
if opt != nil {
if err := opt(inst); err != nil {
return nil, err
}
}
}
if err := viper.UnmarshalKey(inst.configKey, &inst.cfg); err != nil {
return nil, err
}
return inst, nil
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
func (s *Squadron) Cluster(name string) (squadron.Cluster, bool) {
return s.cfg.Cluster(name)
}
func (s *Squadron) Exists(name string) bool {
if _, err := os.Stat(filepath.Join(s.cfg.Path, name, "squadron.yaml")); err != nil {
return false
}
return true
}
func (s *Squadron) UnitExists(ctx context.Context, squadron, cluster, fleet, name string, override bool) bool {
units, _ := s.ListUnits(ctx, squadron, cluster, fleet, override)
return slices.Contains(units, name)
}
func (s *Squadron) List() ([]string, error) {
files, err := os.ReadDir(s.cfg.Path)
if err != nil {
return nil, err
}
var results []string
for _, value := range files {
if value.IsDir() && !strings.HasPrefix(value.Name(), ".") {
results = append(results, value.Name())
}
}
return results, nil
}
func (s *Squadron) ListUnits(ctx context.Context, squadron, cluster, fleet string, override bool) ([]string, error) {
var units []string
files := strings.Join(s.GetFiles(squadron, cluster, fleet, override), ",")
out, err := shell.New(ctx, s.l, "squadron", "list", squadron, "-f", files).
Dir(s.cfg.Path).
Output()
if err != nil {
return nil, errors.WithMessage(err, string(out))
}
for _, line := range strings.Split(string(out), "\n") {
line = stripansi.Strip(line)
if len(line) > 11 && line[8:11] == "─" {
units = append(units, line[11:])
}
}
return units, nil
}
func (s *Squadron) UnitDirs(squadron string) []string {
var units []string
dirs, err := os.ReadDir(filepath.Join(s.cfg.Path, squadron))
if err != nil {
return nil
}
for _, dir := range dirs {
if _, err := os.Stat(filepath.Join(s.cfg.Path, squadron, dir.Name(), "squadron.yaml")); err == nil {
units = append(units, dir.Name())
}
}
return units
}
func (s *Squadron) GetFiles(squadron, cluster, fleet string, override bool) []string {
allFiles := []string{
"squadron.yaml",
"squadron.override.yaml",
fmt.Sprintf("squadron.%s.yaml", fleet),
fmt.Sprintf("squadron.%s.override.yaml", fleet),
fmt.Sprintf("squadron.%s.yaml", cluster),
fmt.Sprintf("squadron.%s.override.yaml", cluster),
fmt.Sprintf("squadron.%s.%s.yaml", cluster, fleet),
fmt.Sprintf("squadron.%s.%s.override.yaml", cluster, fleet),
}
var squadrons []string
if squadron == "" {
if value, err := s.List(); err != nil {
s.l.Debug(err.Error())
return nil
} else {
squadrons = value
}
} else {
squadrons = []string{squadron}
}
for _, value := range squadrons {
allFiles = append(allFiles,
fmt.Sprintf("%s/squadron.yaml", value),
fmt.Sprintf("%s/squadron.override.yaml", value),
fmt.Sprintf("%s/squadron.%s.yaml", value, fleet),
fmt.Sprintf("%s/squadron.%s.override.yaml", value, fleet),
fmt.Sprintf("%s/squadron.%s.yaml", value, cluster),
fmt.Sprintf("%s/squadron.%s.override.yaml", value, cluster),
fmt.Sprintf("%s/squadron.%s.%s.yaml", value, cluster, fleet),
fmt.Sprintf("%s/squadron.%s.%s.override.yaml", value, cluster, fleet),
)
for _, unit := range s.UnitDirs(value) {
allFiles = append(allFiles,
fmt.Sprintf("%s/%s/squadron.yaml", value, unit),
fmt.Sprintf("%s/%s/squadron.override.yaml", value, unit),
fmt.Sprintf("%s/%s/squadron.%s.yaml", value, unit, fleet),
fmt.Sprintf("%s/%s/squadron.%s.override.yaml", value, unit, fleet),
fmt.Sprintf("%s/%s/squadron.%s.yaml", value, unit, cluster),
fmt.Sprintf("%s/%s/squadron.%s.override.yaml", value, unit, cluster),
fmt.Sprintf("%s/%s/squadron.%s.%s.yaml", value, unit, cluster, fleet),
fmt.Sprintf("%s/%s/squadron.%s.%s.override.yaml", value, unit, cluster, fleet),
)
}
}
var files []string
for _, file := range allFiles {
if !override && strings.Contains(file, ".override.") {
continue
}
if _, err := os.Stat(filepath.Join(s.cfg.Path, file)); err == nil {
files = append(files, file)
}
}
return files
}

1
go.mod
View File

@ -6,6 +6,7 @@ replace github.com/c-bata/go-prompt v0.2.6 => github.com/franklinkim/go-prompt v
require (
github.com/1Password/connect-sdk-go v1.5.3
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/c-bata/go-prompt v0.2.6
github.com/cloudrecipes/packagejson v1.0.0
github.com/digitalocean/godo v1.107.0

2
go.sum
View File

@ -19,6 +19,8 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=

View File

@ -18,7 +18,7 @@ type (
Command struct {
l log.Logger
kubectl *kubectl.Kubectl
squadron *squadron.Squadron
squadron squadron.Squadron
commandTree tree.Root
namespaceFn NamespaceFn
}
@ -40,7 +40,7 @@ func CommandWithNamespaceFn(v NamespaceFn) CommandOption {
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewCommand(l log.Logger, kubectl *kubectl.Kubectl, squadron *squadron.Squadron, opts ...CommandOption) *Command {
func NewCommand(l log.Logger, kubectl *kubectl.Kubectl, squadron squadron.Squadron, opts ...CommandOption) *Command {
inst := &Command{
l: l.Named("stern"),
kubectl: kubectl,