From 812bf4a81eff861736e218dc0d0c66b55a828852 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 6 Mar 2020 13:35:56 +0100 Subject: [PATCH] Adopt cobra best practices for setting up commands --- cmd/common.go | 23 +------- cmd/history.go | 70 ++++++++++++----------- cmd/{orphan.go => orphans.go} | 74 +++++++++++++------------ cmd/{orphan_test.go => orphans_test.go} | 0 cmd/root.go | 74 +++++++++++++++++++++++++ main.go | 54 +----------------- pkg/git/git.go | 22 ++++++++ 7 files changed, 178 insertions(+), 139 deletions(-) rename cmd/{orphan.go => orphans.go} (64%) rename cmd/{orphan_test.go => orphans_test.go} (100%) create mode 100644 cmd/root.go diff --git a/cmd/common.go b/cmd/common.go index 324f912..d0db3bc 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "github.com/appuio/image-cleanup/pkg/git" "github.com/appuio/image-cleanup/pkg/openshift" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -20,6 +19,9 @@ type ( SortCriteria string } ) +var ( + gitOptions = GitOptions{} +) func DeleteImages(imageTags []string, imageName string, namespace string) { for _, inactiveTag := range imageTags { @@ -46,22 +48,3 @@ func PrintImageTags(cmd *cobra.Command, imageTags []string) { } } -func getGitCandidateList(o *GitOptions) []string { - logEvent := log.WithFields(log.Fields{ - "GitRepoPath": o.RepoPath, - "CommitLimit": o.CommitLimit, - }) - if o.Tag { - candidates, err := git.GetTags(o.RepoPath, o.CommitLimit, git.SortOption(o.SortCriteria)) - if err != nil { - logEvent.WithError(err).Fatal("Retrieving commit tags failed.") - } - return candidates - } else { - candidates, err := git.GetCommitHashes(o.RepoPath, o.CommitLimit) - if err != nil { - logEvent.WithError(err).Fatal("Retrieving commit hashes failed.") - } - return candidates - } -} diff --git a/cmd/history.go b/cmd/history.go index c86eb8e..0ac30da 100644 --- a/cmd/history.go +++ b/cmd/history.go @@ -14,44 +14,48 @@ type HistoryCleanupOptions struct { Force bool Keep int ImageRepository string - Namespace string } -// NewHistoryCleanupCommand creates a cobra command to clean up images by comparing the git commit history. -func NewHistoryCleanupCommand() *cobra.Command { - historyCleanupOptions := HistoryCleanupOptions{} - gitOptions := GitOptions{} - cmd := &cobra.Command{ +var ( + historyCleanupOptions = HistoryCleanupOptions{} + historyCmd = &cobra.Command{ Use: "history", Aliases: []string{"hist"}, Short: "Clean up excessive image tags", Long: `Clean up excessive image tags matching the commit hashes (prefix) of the git repository`, Run: func(cmd *cobra.Command, args []string) { - validateFlagCombinationInput(&gitOptions) - ExecuteHistoryCleanupCommand(cmd, &historyCleanupOptions, &gitOptions, args) + validateFlagCombinationInput() + ExecuteHistoryCleanupCommand(cmd, args) }, } - cmd.Flags().BoolVarP(&historyCleanupOptions.Force, "force", "f", false, "Confirm deletion of image tags.") - cmd.Flags().IntVarP(&gitOptions.CommitLimit, "git-commit-limit", "l", 0, +) + +func init() { + rootCmd.AddCommand(historyCmd) + historyCmd.Flags().BoolVarP(&historyCleanupOptions.Force, "force", "f", false, "Confirm deletion of image tags.") + historyCmd.Flags().IntVarP(&gitOptions.CommitLimit, "git-commit-limit", "l", 0, "Only look at the first commits to compare with image tags. Use 0 (zero) for all commits. Limited effect if repo is a shallow clone.") - cmd.Flags().StringVarP(&gitOptions.RepoPath, "git-repo-path", "p", ".", "Path to Git repository.") - cmd.Flags().StringVarP(&historyCleanupOptions.ImageRepository, imageRepositoryCliFlag, "i", "", "Image repository in form of .") - cmd.Flags().IntVarP(&historyCleanupOptions.Keep, "keep", "k", 10, "Keep most current images.") - cmd.Flags().BoolVarP(&gitOptions.Tag, "tags", "t", false, "Compare with Git tags instead of commit hashes.") - cmd.Flags().StringVar(&gitOptions.SortCriteria, "sort", string(git.SortOptionVersion), + historyCmd.Flags().StringVarP(&gitOptions.RepoPath, "git-repo-path", "p", ".", "Path to Git repository.") + historyCmd.Flags().StringVarP(&historyCleanupOptions.ImageRepository, imageRepositoryCliFlag, "i", "", "Image repository in form of .") + historyCmd.Flags().IntVarP(&historyCleanupOptions.Keep, "keep", "k", 10, "Keep most current images.") + historyCmd.Flags().BoolVarP(&gitOptions.Tag, "tags", "t", false, "Compare with Git tags instead of commit hashes.") + historyCmd.Flags().StringVar(&gitOptions.SortCriteria, "sort", string(git.SortOptionVersion), fmt.Sprintf("Sort git tags by criteria. Only effective with --tags. Allowed values: %s", []git.SortOption{git.SortOptionVersion, git.SortOptionAlphabetic})) - cmd.MarkFlagRequired("image-repository") - return cmd + historyCmd.MarkFlagRequired("image-repository") + } -func ExecuteHistoryCleanupCommand(cmd *cobra.Command, o *HistoryCleanupOptions, gitOptions *GitOptions, args []string) { +func ExecuteHistoryCleanupCommand(cmd *cobra.Command, args []string) { - imageStreamObjectTags, err := openshift.GetImageStreamTags(o.Namespace, o.ImageRepository) + namespace, image, _ := splitNamespaceAndImagestream(historyCleanupOptions.ImageRepository) + + imageStreamObjectTags, err := openshift.GetImageStreamTags(namespace, image) if err != nil { log.WithError(err). WithFields(log.Fields{ - "ImageRepository": o.Namespace, - "ImageName": o.ImageRepository}). + "ImageRepository": namespace, + "ImageName": image, + }). Fatal("Could not retrieve image stream.") } @@ -60,41 +64,43 @@ func ExecuteHistoryCleanupCommand(cmd *cobra.Command, o *HistoryCleanupOptions, imageStreamTags = append(imageStreamTags, imageTag.Tag) } - var matchOption cleanup.MatchOption + matchOption := cleanup.MatchOptionDefault if gitOptions.Tag { matchOption = cleanup.MatchOptionExact } - gitCandidates := getGitCandidateList(gitOptions) + gitCandidates := git.GetGitCandidateList(&gitOptions) var matchingTags = cleanup.GetMatchingTags(&gitCandidates, &imageStreamTags, matchOption) - activeImageStreamTags, err := openshift.GetActiveImageStreamTags(o.Namespace, o.ImageRepository, imageStreamTags) + activeImageStreamTags, err := openshift.GetActiveImageStreamTags(namespace, image, imageStreamTags) if err != nil { log.WithError(err). WithFields(log.Fields{ - "ImageRepository": o.Namespace, - "ImageName": o.ImageRepository, + "ImageRepository": namespace, + "ImageName": image, "imageStreamTags": imageStreamTags}). Fatal("Could not retrieve active image stream tags.") } inactiveTags := cleanup.GetInactiveImageTags(&activeImageStreamTags, &matchingTags) - - inactiveTags = cleanup.LimitTags(&inactiveTags, o.Keep) + inactiveTags = cleanup.LimitTags(&inactiveTags, historyCleanupOptions.Keep) PrintImageTags(cmd, inactiveTags) - if o.Force { - DeleteImages(inactiveTags, o.ImageRepository, o.Namespace) + if historyCleanupOptions.Force { + DeleteImages(inactiveTags, image, namespace) } else { log.Info("--force was not specified. Nothing has been deleted.") } } -func validateFlagCombinationInput(gitOptions *GitOptions) { +func validateFlagCombinationInput() { if gitOptions.Tag && !git.IsValidSortValue(gitOptions.SortCriteria) { - log.WithField("sort_criteria", gitOptions.SortCriteria).Fatal("Invalid sort criteria.") + log.WithFields(log.Fields{ + "error": "invalid sort criteria", + "sort": gitOptions.SortCriteria, + }).Fatal("Could not parse sort criteria.") } } diff --git a/cmd/orphan.go b/cmd/orphans.go similarity index 64% rename from cmd/orphan.go rename to cmd/orphans.go index 58cf6c8..07af1c3 100644 --- a/cmd/orphan.go +++ b/cmd/orphans.go @@ -29,55 +29,59 @@ This command deletes images that are not found in the git history.` orphanOlderThanCliFlag = "older-than" ) -// NewOrphanCleanupCommand creates a cobra command to clean up images by comparing the git commit history. It removes any -// image tags that are not found in the git history by given criteria. -func NewOrphanCleanupCommand() *cobra.Command { - o := OrphanCleanupOptions{} - gitOptions := GitOptions{} - cmd := &cobra.Command{ +var ( + orphanCleanupOptions = OrphanCleanupOptions{} + // orphanCmd represents a cobra command to clean up images by comparing the git commit history. It removes any + // image tags that are not found in the git history by given criteria. + orphanCmd = &cobra.Command{ Use: "orphans", - Aliases: []string{"orph"}, Short: "Clean up unknown image tags", Long: orphanCommandLongDescription, + Aliases: []string{"orph"}, RunE: func(cmd *cobra.Command, args []string) error { - validateOrphanCommandInput(&o, &gitOptions) - return ExecuteOrphanCleanupCommand(cmd, &o, args) + validateOrphanCommandInput() + return ExecuteOrphanCleanupCommand(cmd, args) }, } - cmd.Flags().BoolVarP(&o.Force, "force", "f", false, "Confirm deletion of image tags.") - cmd.Flags().IntVarP(&gitOptions.CommitLimit, "git-commit-limit", "l", 0, +) + +func init() { + rootCmd.AddCommand(orphanCmd) + + orphanCmd.Flags().BoolVarP(&orphanCleanupOptions.Force, "force", "f", false, "Confirm deletion of image tags.") + orphanCmd.Flags().IntVarP(&gitOptions.CommitLimit, "git-commit-limit", "l", 0, "Only look at the first commits to compare with tags. Use 0 (zero) for all commits. Limited effect if repo is a shallow clone.") - cmd.Flags().StringVarP(&gitOptions.RepoPath, "git-repo-path", "p", ".", "Path to Git repository") - cmd.Flags().StringVarP(&o.ImageRepository, imageRepositoryCliFlag, "i", "", "Image repository (e.g. namespace/repo)") - cmd.Flags().BoolVarP(&gitOptions.Tag, "tags", "t", false, + orphanCmd.Flags().StringVarP(&gitOptions.RepoPath, "git-repo-path", "p", ".", "Path to Git repository") + orphanCmd.Flags().StringVarP(&orphanCleanupOptions.ImageRepository, imageRepositoryCliFlag, "i", "", "Image repository (e.g. namespace/repo)") + orphanCmd.Flags().BoolVarP(&gitOptions.Tag, "tags", "t", false, "Instead of comparing commit history, it will compare git tags with the existing image tags, removing any image tags that do not match") - cmd.Flags().StringVar(&gitOptions.SortCriteria, "sort", string(git.SortOptionVersion), + orphanCmd.Flags().StringVar(&gitOptions.SortCriteria, "sort", string(git.SortOptionVersion), fmt.Sprintf("Sort git tags by criteria. Only effective with --tags. Allowed values: [%s, %s]", git.SortOptionVersion, git.SortOptionAlphabetic)) - cmd.Flags().StringVar(&o.OlderThan, orphanOlderThanCliFlag, "2mo", - "delete images that are older than the duration. Ex.: [1y2mo3w4d5h6m7s]") - cmd.Flags().StringVarP(&o.OrphanDeletionRegex, orphanDeletionPatternCliFlag, "r", "^[a-z0-9]{40}$", + orphanCmd.Flags().StringVar(&orphanCleanupOptions.OlderThan, orphanOlderThanCliFlag, "2mo", + "Delete images that are older than the duration. Ex.: [1y2mo3w4d5h6m7s]") + orphanCmd.Flags().StringVarP(&orphanCleanupOptions.OrphanDeletionRegex, orphanDeletionPatternCliFlag, "r", "^[a-z0-9]{40}$", "Delete images that match the regex, defaults to matching Git SHA commits") - cmd.MarkFlagRequired("image-repository") - return cmd + orphanCmd.MarkFlagRequired("image-repository") + } -func validateOrphanCommandInput(o *OrphanCleanupOptions, gitOptions *GitOptions) { +func validateOrphanCommandInput() { - if _, _, err := splitNamespaceAndImagestream(o.ImageRepository); err != nil { + if _, _, err := splitNamespaceAndImagestream(orphanCleanupOptions.ImageRepository); err != nil { log.WithError(err). - WithField(imageRepositoryCliFlag, o.ImageRepository). + WithField(imageRepositoryCliFlag, orphanCleanupOptions.ImageRepository). Fatal("Could not parse image repository.") } - if _, err := parseOrphanDeletionRegex(o.OrphanDeletionRegex); err != nil { + if _, err := parseOrphanDeletionRegex(orphanCleanupOptions.OrphanDeletionRegex); err != nil { log.WithError(err). - WithField(orphanDeletionPatternCliFlag, o.OrphanDeletionRegex). + WithField(orphanDeletionPatternCliFlag, orphanCleanupOptions.OrphanDeletionRegex). Fatal("Could not parse orphan deletion pattern.") } - if _, err := parseCutOffDateTime(o.OlderThan); err != nil { + if _, err := parseCutOffDateTime(orphanCleanupOptions.OlderThan); err != nil { log.WithError(err). - WithField(orphanOlderThanCliFlag, o.OlderThan). + WithField(orphanOlderThanCliFlag, orphanCleanupOptions.OlderThan). Fatal("Could not parse cut off date.") } @@ -90,22 +94,22 @@ func validateOrphanCommandInput(o *OrphanCleanupOptions, gitOptions *GitOptions) } -func ExecuteOrphanCleanupCommand(cmd *cobra.Command, o *OrphanCleanupOptions, gitOptions *GitOptions, args []string) error { +func ExecuteOrphanCleanupCommand(cmd *cobra.Command, args []string) error { - gitCandidates := getGitCandidateList(gitOptions) + gitCandidates := git.GetGitCandidateList(&gitOptions) - namespace, imageName, err := splitNamespaceAndImagestream(o.ImageRepository) + namespace, imageName, _ := splitNamespaceAndImagestream(orphanCleanupOptions.ImageRepository) imageStreamObjectTags, err := openshift.GetImageStreamTags(namespace, imageName) if err != nil { log.WithError(err). WithFields(log.Fields{ - "ImageRepository": o.ImageRepository, + "ImageRepository": orphanCleanupOptions.ImageRepository, }). Fatal("Could not retrieve image stream.") } - cutOffDateTime, _ := parseCutOffDateTime(o.OlderThan) + cutOffDateTime, _ := parseCutOffDateTime(orphanCleanupOptions.OlderThan) imageStreamTags := cleanup.FilterImageTagsByTime(&imageStreamObjectTags, cutOffDateTime) var matchOption cleanup.MatchOption @@ -113,7 +117,7 @@ func ExecuteOrphanCleanupCommand(cmd *cobra.Command, o *OrphanCleanupOptions, gi matchOption = cleanup.MatchOptionExact } - orphanIncludeRegex, _ := parseOrphanDeletionRegex(o.OrphanDeletionRegex) + orphanIncludeRegex, _ := parseOrphanDeletionRegex(orphanCleanupOptions.OrphanDeletionRegex) var matchingTags []string matchingTags = cleanup.GetOrphanImageTags(&gitCandidates, &imageStreamTags, matchOption) matchingTags = cleanup.FilterByRegex(&imageStreamTags, orphanIncludeRegex) @@ -122,7 +126,7 @@ func ExecuteOrphanCleanupCommand(cmd *cobra.Command, o *OrphanCleanupOptions, gi if err != nil { log.WithError(err). WithFields(log.Fields{ - "ImageRepository": o.ImageRepository, + "ImageRepository": orphanCleanupOptions.ImageRepository, "ImageName": imageName, "imageStreamTags": imageStreamTags}). Fatal("Could not retrieve active image stream tags.") @@ -133,7 +137,7 @@ func ExecuteOrphanCleanupCommand(cmd *cobra.Command, o *OrphanCleanupOptions, gi PrintImageTags(cmd, inactiveImageTags) - if o.Force { + if orphanCleanupOptions.Force { DeleteImages(inactiveImageTags, imageName, namespace) } else { log.Info("--force was not specified. Nothing has been deleted.") diff --git a/cmd/orphan_test.go b/cmd/orphans_test.go similarity index 100% rename from cmd/orphan_test.go rename to cmd/orphans_test.go diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..54828d8 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "io/ioutil" + "os" +) + +// Options is a struct holding the options of the root command +type ( + Options struct { + LogLevel string + Batch bool + Verbose bool + } +) + +var ( + // rootCmd represents the base command when called without any subcommands + rootCmd = &cobra.Command{ + Use: "image-cleanup", + Short: "Cleans up images tags on remote registries", + } + options = &Options{} +) + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().StringVar(&options.LogLevel, "logLevel", "info", "Log level to use") + rootCmd.PersistentFlags().BoolVarP(&options.Verbose, "verbose", "v", false, "Shorthand for --logLevel debug") + rootCmd.PersistentFlags().BoolVarP(&options.Batch, "batch", "b", false, "Use Batch mode (disables logging, prints deleted images only)") + cobra.OnInitialize(initConfig) + +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + + log.SetFormatter(&log.TextFormatter{ + FullTimestamp: true, + }) + + if options.Batch { + log.SetOutput(ioutil.Discard) + } else { + log.SetOutput(os.Stderr) + } + if options.Verbose { + log.SetLevel(log.DebugLevel) + } else { + level, err := log.ParseLevel(options.LogLevel) + if err != nil { + log.WithField("error", err).Warn("Using info level.") + log.SetLevel(log.InfoLevel) + } else { + log.SetLevel(level) + } + } +} + +// SetVersion sets the version string in the help messages +func SetVersion(version string) { + rootCmd.Version = version +} diff --git a/main.go b/main.go index 735f513..cfd2857 100644 --- a/main.go +++ b/main.go @@ -3,65 +3,15 @@ package main import ( "fmt" "github.com/appuio/image-cleanup/cmd" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "io/ioutil" - "os" ) var ( version = "unknown" commit = "dirty" date = "today" - rootCmd = &cobra.Command{ - Use: "image-cleanup", - Short: "Cleans up images tags on remote registries", - Version: fmt.Sprintf("%s, commit %s, date %s", version, commit, date), - } - options = &Options{} ) -// Options is a struct holding the options of the root command -type Options struct { - LogLevel string - Batch bool -} - -func initializeCmd() { - configureLogging() -} - func main() { - rootCmd.PersistentFlags().StringVarP(&options.LogLevel, "logLevel", "v", "info", "Log level to use") - rootCmd.PersistentFlags().BoolVarP(&options.Batch, "batch", "b", false, "Use Batch mode (disables logging, prints deleted images only)") - rootCmd.AddCommand( - cmd.NewHistoryCleanupCommand(), - cmd.NewOrphanCleanupCommand(), - ) - cobra.OnInitialize(initializeCmd) - if err := rootCmd.Execute(); err != nil { - log.WithError(err).Error("Command failed with error.") - os.Exit(1) - } -} - -func configureLogging() { - - log.SetFormatter(&log.TextFormatter{ - FullTimestamp: true, - }) - - if options.Batch { - log.SetOutput(ioutil.Discard) - } else { - log.SetOutput(os.Stderr) - } - - level, err := log.ParseLevel(options.LogLevel) - if err != nil { - log.WithField("error", err).Warn("Using info level.") - log.SetLevel(log.InfoLevel) - } else { - log.SetLevel(level) - } + cmd.SetVersion(fmt.Sprintf("%s, commit %s, date %s", version, commit, date)) + cmd.Execute() } diff --git a/pkg/git/git.go b/pkg/git/git.go index fa57213..77b3347 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -1,6 +1,8 @@ package git import ( + "github.com/appuio/image-cleanup/cmd" + "github.com/sirupsen/logrus" "io" "strings" @@ -69,3 +71,23 @@ func GetTags(repoPath string, tagLimit int, sortTagBy SortOption) ([]string, err return sortTags(commitTags, sortTagBy) } + +func GetGitCandidateList(o *cmd.GitOptions) []string { + logEvent := logrus.WithFields(logrus.Fields{ + "GitRepoPath": o.RepoPath, + "CommitLimit": o.CommitLimit, + }) + if o.Tag { + candidates, err := GetTags(o.RepoPath, o.CommitLimit, SortOption(o.SortCriteria)) + if err != nil { + logEvent.WithError(err).Fatal("Retrieving commit tags failed.") + } + return candidates + } else { + candidates, err := GetCommitHashes(o.RepoPath, o.CommitLimit) + if err != nil { + logEvent.WithError(err).Fatal("Retrieving commit hashes failed.") + } + return candidates + } +}