Skip to content

Commit

Permalink
feat: plugin implementation (#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
kbudde authored Dec 22, 2023
1 parent 59d8b0f commit f23a41b
Show file tree
Hide file tree
Showing 14 changed files with 432 additions and 158 deletions.
24 changes: 10 additions & 14 deletions cmd/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,16 @@ import (
"github.com/mykso/myks/internal/myks"
)

func init() {
cmd := &cobra.Command{
Use: "all",
Short: "Run sync and render",
Long: "Run sync and render",
Annotations: map[string]string{
ANNOTATION_SMART_MODE: ANNOTATION_TRUE,
},
Run: func(cmd *cobra.Command, args []string) {
RunAllCmd()
},
}

rootCmd.AddCommand(cmd)
var allCmd = &cobra.Command{
Use: "all",
Short: "Run sync and render",
Long: "Run sync and render",
Annotations: map[string]string{
ANNOTATION_SMART_MODE: ANNOTATION_TRUE,
},
Run: func(cmd *cobra.Command, args []string) {
RunAllCmd()
},
}

func RunAllCmd() {
Expand Down
4 changes: 2 additions & 2 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/mykso/myks/internal/myks"
)

func init() {
func newInitCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Initialize new myks project",
Expand Down Expand Up @@ -53,5 +53,5 @@ func init() {
}
cmd.Flags().StringSlice("components", componentsDefault, "components to initialize")

rootCmd.AddCommand(cmd)
return cmd
}
83 changes: 83 additions & 0 deletions cmd/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package cmd

import (
"os"
"path/filepath"

"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/mykso/myks/internal/myks"
)

const pluginPrefix = "myks-"

// findPlugins searches for plugins in the PATH and in configured plugin-sources
func findPlugins() []myks.Plugin {
path := filepath.SplitList(os.Getenv("PATH"))
pluginsPath := myks.FindPluginsInPaths(path, pluginPrefix)

sources := viper.GetStringSlice("plugin-sources")
for i, source := range sources {
sources[i] = os.ExpandEnv(source) // supports $HOME but not ~
}
pluginsLocal := myks.FindPluginsInPaths(sources, "")

return append(pluginsPath, pluginsLocal...)
}

func addPlugins(cmd *cobra.Command) {
plugins := findPlugins()

uniquePlugins := make(map[string]myks.Plugin)
for _, plugin := range plugins {
uniquePlugins[plugin.Name()] = plugin
}

if len(uniquePlugins) > 0 {
cmd.AddGroup(&cobra.Group{ID: "Plugins", Title: "Plugin Subcommands:"})
}

for _, plugin := range uniquePlugins {
cmd.AddCommand(newPluginCmd(plugin))
}
}

func newPluginCmd(plugin myks.Plugin) *cobra.Command {
return &cobra.Command{
Use: plugin.Name(),
Short: "Execute " + plugin.Name(),
Long: "Execute" + plugin.Name(),
GroupID: "Plugins",
RunE: func(cmd *cobra.Command, args []string) error {
splitAt := cmd.ArgsLenAtDash()
if splitAt == -1 {
splitAt = len(args)
}
myksArgs, pluginArgs := args[:splitAt], args[splitAt:]

if err := initTargetEnvsAndApps(cmd, myksArgs); err != nil {
return err
}
g := myks.New(".")

if err := g.ValidateRootDir(); err != nil {
log.Fatal().Err(err).Msg("Root directory is not suitable for myks")
return err
}

if err := g.Init(asyncLevel, envAppMap); err != nil {
log.Fatal().Err(err).Msg("Unable to initialize globe")
return err
}

if err := g.ExecPlugin(asyncLevel, plugin, pluginArgs); err != nil {
log.Fatal().Err(err).Msg("Unable to render applications")
return err
}

return nil
},
}
}
4 changes: 2 additions & 2 deletions cmd/print-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
yaml "gopkg.in/yaml.v3"
)

func init() {
func newPrintConfigCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "print-config",
Short: "Print myks configuration",
Expand All @@ -23,5 +23,5 @@ func init() {
fmt.Printf("---\n%s\n", bs)
},
}
rootCmd.AddCommand(cmd)
return cmd
}
60 changes: 28 additions & 32 deletions cmd/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,33 @@ import (
"github.com/mykso/myks/internal/myks"
)

func init() {
cmd := &cobra.Command{
Use: "render",
Short: "Render manifests",
Long: "Render manifests",
Annotations: map[string]string{
ANNOTATION_SMART_MODE: ANNOTATION_TRUE,
},
Run: func(cmd *cobra.Command, args []string) {
g := myks.New(".")

if err := g.ValidateRootDir(); err != nil {
log.Fatal().Err(err).Msg("Root directory is not suitable for myks")
}

if err := g.Init(asyncLevel, envAppMap); err != nil {
log.Fatal().Err(err).Msg("Unable to initialize globe")
}

if err := g.Render(asyncLevel); err != nil {
log.Fatal().Err(err).Msg("Unable to render applications")
}

// Cleaning up only if all environments and applications were processed
if envAppMap == nil {
if err := g.Cleanup(); err != nil {
log.Fatal().Err(err).Msg("Unable to cleanup")
}
var renderCmd = &cobra.Command{
Use: "render",
Short: "Render manifests",
Long: "Render manifests",
Annotations: map[string]string{
ANNOTATION_SMART_MODE: ANNOTATION_TRUE,
},
Run: func(cmd *cobra.Command, args []string) {
g := myks.New(".")

if err := g.ValidateRootDir(); err != nil {
log.Fatal().Err(err).Msg("Root directory is not suitable for myks")
}

if err := g.Init(asyncLevel, envAppMap); err != nil {
log.Fatal().Err(err).Msg("Unable to initialize globe")
}

if err := g.Render(asyncLevel); err != nil {
log.Fatal().Err(err).Msg("Unable to render applications")
}

// Cleaning up only if all environments and applications were processed
if envAppMap == nil {
if err := g.Cleanup(); err != nil {
log.Fatal().Err(err).Msg("Unable to cleanup")
}
},
}

rootCmd.AddCommand(cmd)
}
},
}
141 changes: 37 additions & 104 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package cmd

import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"

aurora "github.com/logrusorgru/aurora/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
Expand All @@ -18,10 +16,8 @@ import (
)

const (
ANNOTATION_SMART_MODE = "feat:smart-mode"
ANNOTATION_TRUE = "true"
MYKS_CONFIG_NAME = ".myks"
MYKS_CONFIG_TYPE = "yaml"
MYKS_CONFIG_NAME = ".myks"
MYKS_CONFIG_TYPE = "yaml"
)

var (
Expand All @@ -30,24 +26,38 @@ var (
asyncLevel int
)

var rootCmd = &cobra.Command{
Use: "myks",
Short: "Myks helps to manage configuration for kubernetes clusters",
Long: `Myks fetches K8s workloads from a variety of sources, e.g. Helm charts or Git Repositories. It renders their respective yaml files to the file system in a structure of environments and their applications.
It supports prototype applications that can be shared between environments and inheritance of configuration from parent environments to their "children".
Myks supports two positional arguments:
- A comma-separated list of environments to render. If you provide "ALL", all environments will be rendered.
- A comma-separated list of applications to render. If you don't provide this argument or provide "ALL", all applications will be rendered.
If you do not provide any positional arguments, myks will run in "Smart Mode". In Smart Mode, myks will only render environments and applications that have changed since the last run.
`,
func NewMyksCmd(version, commit, date string) *cobra.Command {
cmd := newRootCmd(version, commit, date)
cmd.AddCommand(allCmd)
cmd.AddCommand(renderCmd)
cmd.AddCommand(newInitCmd())
cmd.AddCommand(newPrintConfigCmd())
cmd.AddCommand(newSyncCmd())
initConfig()
addPlugins(cmd)
return cmd
}

func init() {
cobra.OnInitialize(initConfig)
func newRootCmd(version, commit, date string) *cobra.Command {
rootCmd := &cobra.Command{
Use: "myks",
Short: "Myks helps to manage configuration for kubernetes clusters",
Long: `Myks fetches K8s workloads from a variety of sources, e.g. Helm charts or Git Repositories. It renders their respective yaml files to the file system in a structure of environments and their applications.
It supports prototype applications that can be shared between environments and inheritance of configuration from parent environments to their "children".
Myks supports two positional arguments:
- A comma-separated list of environments to render. If you provide "ALL", all environments will be rendered.
- A comma-separated list of applications to render. If you don't provide this argument or provide "ALL", all applications will be rendered.
If you do not provide any positional arguments, myks will run in "Smart Mode". In Smart Mode, myks will only render environments and applications that have changed since the last run.
`,
}

rootCmd.Version = fmt.Sprintf(`%s
commit: %s
date: %s`, version, commit, date)

rootCmd.PersistentFlags().StringP("log-level", "l", "info", "set the logging level")

Expand All @@ -71,80 +81,13 @@ func init() {
configHelp := fmt.Sprintf("config file (default is the first %s.%s up the directory tree)", MYKS_CONFIG_NAME, MYKS_CONFIG_TYPE)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", configHelp)

rootCmd.PersistentPreRunE = detectTargetEnvsAndApps
}

func detectTargetEnvsAndApps(cmd *cobra.Command, args []string) (err error) {
// Check positional arguments for Smart Mode:
// 1. Comma-separated list of environment search paths or ALL to search everywhere (default: ALL)
// 2. Comma-separated list of application names or none to process all applications (default: none)

if cmd.Annotations[ANNOTATION_SMART_MODE] != ANNOTATION_TRUE {
log.Debug().Msg("Smart Mode is not supported for this command.")
return
}

switch len(args) {
case 0:
// smart mode requires instantiation of globe object to get the list of environments
// the globe object will not be used later in the process. It is only used to get the list of all environments and their apps.
globeAllEnvsAndApps := myks.New(".")
envAppMap, err = globeAllEnvsAndApps.DetectChangedEnvsAndApps(viper.GetString("smart-mode.base-revision"))
if err != nil {
log.Warn().Err(err).Msg("Unable to run Smart Mode. Rendering everything.")
} else if len(envAppMap) == 0 {
log.Warn().Msg("Smart Mode did not find any changes. Exiting.")
os.Exit(0)
}
case 1:
if args[0] != "ALL" {
envAppMap = make(myks.EnvAppMap)
for _, env := range strings.Split(args[0], ",") {
envAppMap[env] = nil
}
}
case 2:
var appNames []string
if args[1] != "ALL" {
appNames = strings.Split(args[1], ",")
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if cmd.Annotations[ANNOTATION_SMART_MODE] != ANNOTATION_TRUE {
return nil
}

envAppMap = make(myks.EnvAppMap)
if args[0] != "ALL" {
for _, env := range strings.Split(args[0], ",") {
envAppMap[env] = appNames
}
} else {
// TODO: Use Globe.EnvironmentBaseDir instead of the hardcoded key
envAppMap["envs"] = appNames
}

default:
err := errors.New("Too many positional arguments")
log.Error().Err(err).Msg("Unable to parse positional arguments")
return err
}

log.Debug().
Interface("envAppMap", envAppMap).
Msg("Parsed arguments")

if viper.GetBool("smart-mode.only-print") {
fmt.Println(aurora.Bold("\nSmart Mode detected:"))
for env, apps := range envAppMap {
fmt.Printf("→ %s\n", env)
if apps == nil {
fmt.Println(aurora.Bold(" ALL"))
} else {
for _, app := range apps {
fmt.Printf(" %s\n", app)
}
}
}
os.Exit(0)
return initTargetEnvsAndApps(cmd, args)
}

return nil
return rootCmd
}

func initConfig() {
Expand Down Expand Up @@ -193,13 +136,3 @@ func initLogger() {
log.Info().Msgf("Setting log level to: %s", logLevel)
zerolog.SetGlobalLevel(logLevel)
}

func SetVersionInfo(version, commit, date string) {
rootCmd.Version = fmt.Sprintf(`%s
commit: %s
date: %s`, version, commit, date)
}

func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
Loading

0 comments on commit f23a41b

Please sign in to comment.