diff --git a/cmd/all.go b/cmd/all.go index 13ec0886..e30def3b 100644 --- a/cmd/all.go +++ b/cmd/all.go @@ -37,8 +37,8 @@ func RunAllCmd() { // Cleaning up only if all environments and applications were processed if envAppMap == nil { - if err := g.CleanupRenderedManifests(); err != nil { - log.Fatal().Err(err).Msg("Unable to cleanup") + if err := g.CleanupRenderedManifests(false); err != nil { + log.Fatal().Err(err).Msg("Unable to cleanup rendered manifests") } } } diff --git a/cmd/cleanup.go b/cmd/cleanup.go index 1c85cab1..e6540841 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -7,23 +7,74 @@ import ( "github.com/mykso/myks/internal/myks" ) -var cleanUpCmd = &cobra.Command{ - Use: "cleanup", - Short: "Cleanup obsolete manifests", - Long: "Cleanup obsolete manifests", - 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 myks's globe") - } - - if err := g.CleanupRenderedManifests(); err != nil { - log.Fatal().Err(err).Msg("Unable to cleanup") - } - }, +const cleanupCmdLongHelp = `Cleanup obsolete manifests and cache entries. + +This command will cleanup rendered manifests and cache entries that are no longer needed. +By default, it will cleanup both rendered manifests and cache entries, but you can control +what to cleanup with the flags --manifests and --cache. + +Examples: + # Cleanup all rendered manifests and cache entries + myks cleanup + + # Cleanup only rendered manifests + myks cleanup --manifests + + # List cache entrise that would be cleaned up + myks cleanup --cache --dry-run +` + +func newCleanupCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "cleanup", + Short: "Cleanup obsolete manifests and cache entries", + Long: cleanupCmdLongHelp, + Run: func(cmd *cobra.Command, args []string) { + dryRun, _ := readFlagBool(cmd, "dry-run") + + if dryRun { + log.Info().Msg("Running in dry-run mode") + } + + modeManifests, modeManifestsSet := readFlagBool(cmd, "manifests") + modeCache, modeCacheSet := readFlagBool(cmd, "cache") + + if !modeManifestsSet && !modeCacheSet { + modeManifests = true + modeCache = true + } + + if !modeManifests && !modeCache { + log.Fatal().Msg("Nothing to cleanup") + } + + 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 myks's globe") + } + + if modeManifests { + if err := g.CleanupRenderedManifests(dryRun); err != nil { + log.Fatal().Err(err).Msg("Unable to cleanup rendered manifests") + } + } + + if modeCache { + if err := g.CleanupObsoleteCacheEntries(dryRun); err != nil { + log.Fatal().Err(err).Msg("Unable to cleanup cache entries") + } + } + }, + } + + cmd.Flags().Bool("dry-run", false, "print what would be cleaned up without actually cleaning up") + cmd.Flags().Bool("manifests", false, "cleanup rendered manifests") + cmd.Flags().Bool("cache", false, "cleanup cache entries") + + return cmd } diff --git a/cmd/render.go b/cmd/render.go index 1c8256be..9dde34a0 100644 --- a/cmd/render.go +++ b/cmd/render.go @@ -31,8 +31,8 @@ var renderCmd = &cobra.Command{ // Cleaning up only if all environments and applications were processed if envAppMap == nil { - if err := g.CleanupRenderedManifests(); err != nil { - log.Fatal().Err(err).Msg("Unable to cleanup") + if err := g.CleanupRenderedManifests(false); err != nil { + log.Fatal().Err(err).Msg("Unable to cleanup rendered manifests") } } }, diff --git a/cmd/root.go b/cmd/root.go index 7be80c61..ba746eaf 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -33,7 +33,7 @@ func NewMyksCmd(version, commit, date string) *cobra.Command { cmd := newRootCmd(version, commit, date) cmd.AddCommand(allCmd) cmd.AddCommand(renderCmd) - cmd.AddCommand(cleanUpCmd) + cmd.AddCommand(newCleanupCmd()) cmd.AddCommand(newInitCmd()) cmd.AddCommand(newPrintConfigCmd()) cmd.AddCommand(newSyncCmd()) diff --git a/cmd/util.go b/cmd/util.go index 10d71a3b..b68d82ea 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -4,6 +4,7 @@ import ( "path/filepath" "strings" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/mykso/myks/internal/myks" @@ -45,3 +46,15 @@ func getAppNamesForEnv(globe *myks.Globe, envPath string) []string { } return []string{} } + +// readFlagBool reads a boolean flag from a cobra command and returns the value and whether the flag was set +func readFlagBool(cmd *cobra.Command, name string) (bool, bool) { + flag, err := cmd.Flags().GetBool(name) + if err != nil { + log.Fatal().Err(err).Msg("Failed to read flag") + // This should never happen + return false, false + } + + return flag, cmd.Flags().Changed(name) +} diff --git a/internal/myks/globe.go b/internal/myks/globe.go index 82fc74aa..7326751f 100644 --- a/internal/myks/globe.go +++ b/internal/myks/globe.go @@ -266,7 +266,7 @@ func (g *Globe) ExecPlugin(asyncLevel int, p Plugin, args []string) error { // CleanupRenderedManifests discovers rendered environments that are not known to the Globe struct and removes them. // This function should be only run when the Globe is not restricted by a list of environments. -func (g *Globe) CleanupRenderedManifests() error { +func (g *Globe) CleanupRenderedManifests(dryRun bool) error { legalEnvs := map[string]bool{} for _, env := range g.environments { legalEnvs[env.Id] = true @@ -284,12 +284,18 @@ func (g *Globe) CleanupRenderedManifests() error { } for _, file := range files { - _, ok := legalEnvs[file.Name()] - if file.IsDir() && !ok { + if !file.IsDir() { + log.Warn().Str("file", dir+"/"+file.Name()).Msg("Skipping non-directory entry") + continue + } + if _, ok := legalEnvs[file.Name()]; !ok { + if dryRun { + log.Info().Str("dir", dir+"/"+file.Name()).Msg("Would cleanup rendered environment directory") + continue + } log.Debug().Str("dir", dir+"/"+file.Name()).Msg("Cleanup rendered environment directory") fullPath := filepath.Join(dirPath, file.Name()) - err = os.RemoveAll(fullPath) - if err != nil { + if err := os.RemoveAll(fullPath); err != nil { log.Warn().Str("dir", fullPath).Msg("Failed to remove directory") } } @@ -299,6 +305,52 @@ func (g *Globe) CleanupRenderedManifests() error { return nil } +// CleanupObsoleteCacheEntries removes cache entries that are not used by any application. +// This function should be only run when the Globe is not restricted by a list of environments. +func (g *Globe) CleanupObsoleteCacheEntries(dryRun bool) error { + validCacheDirs := map[string]bool{} + for _, env := range g.environments { + for _, app := range env.Applications { + linksMap, err := app.getLinksMap() + if err != nil { + return err + } + for _, cacheName := range linksMap { + validCacheDirs[cacheName] = true + } + } + } + + cacheDir := filepath.Join(g.ServiceDirName, g.VendirCache) + cacheEntries, err := os.ReadDir(cacheDir) + if os.IsNotExist(err) { + log.Debug().Str("dir", cacheDir).Msg("Skipping cleanup of non-existing directory") + return nil + } else if err != nil { + return fmt.Errorf("unable to read dir: %w", err) + } + + for _, entry := range cacheEntries { + if !entry.IsDir() { + log.Warn().Str("file", cacheDir+"/"+entry.Name()).Msg("Skipping non-directory entry") + continue + } + if _, ok := validCacheDirs[entry.Name()]; !ok { + if dryRun { + log.Info().Str("dir", cacheDir+"/"+entry.Name()).Msg("Would cleanup cache entry") + continue + } + log.Debug().Str("dir", cacheDir+"/"+entry.Name()).Msg("Cleanup cache entry") + fullPath := filepath.Join(cacheDir, entry.Name()) + if err = os.RemoveAll(fullPath); err != nil { + log.Warn().Str("dir", fullPath).Msg("Failed to remove directory") + } + } + } + + return nil +} + // dumpConfigAsYaml dumps the globe config as yaml to a file and returns the file name func (g *Globe) dumpConfigAsYaml() (string, error) { configData := struct {