Skip to content

Commit

Permalink
Remove listing features when insufficient input is given
Browse files Browse the repository at this point in the history
The listing of labels and image streams can be done with kubectl/oc.
There is no reason for us to maintain and reinvent the wheel for already
existing and battle-tested tools.

Signed-off-by: Chris <github.account@chrigel.net>
ccremer committed Aug 5, 2020
1 parent 3ec37ec commit 55aaafe
Showing 13 changed files with 82 additions and 244 deletions.
46 changes: 10 additions & 36 deletions cmd/common.go
Original file line number Diff line number Diff line change
@@ -37,23 +37,6 @@ func PrintImageTags(imageTags []string, imageName string, namespace string) {
}
}

// PrintResources prints the given resource line by line. In batch mode, only the resource is printed, otherwise default
// log with info level
func PrintResources(resources []metav1.ObjectMeta, kind string) {
if len(resources) == 0 {
log.Info("Nothing found to be deleted.")
}
if config.Log.Batch {
for _, resource := range resources {
fmt.Println(kind + ": " + resource.GetName())
}
} else {
for _, resource := range resources {
log.Infof("Found %s candidate: %s/%s", kind, resource.Namespace, resource.GetName())
}
}
}

// addCommonFlagsForGit sets up the delete flag, as well as the common git flags. Adding the flags to the root cmd would make those
// global, even for commands that do not need them, which might be overkill.
func addCommonFlagsForGit(cmd *cobra.Command, defaults *cfg.Configuration) {
@@ -67,27 +50,18 @@ func addCommonFlagsForGit(cmd *cobra.Command, defaults *cfg.Configuration) {
fmt.Sprintf("Sort git tags by criteria. Only effective with --tags. Allowed values: [%s, %s]", git.SortOptionVersion, git.SortOptionAlphabetic))
}

func listImages() error {
namespace := config.Namespace
imageStreams, err := openshift.ListImageStreams(namespace)
if err != nil {
return err
}
imageNames := []string{}
for _, image := range imageStreams {
imageNames = append(imageNames, image.Name)
}
log.WithFields(log.Fields{
"\n - namespace": namespace,
"\n - 📺 images": imageNames,
}).Info("Please select an image. The following images are available:")
return nil
}

//GetListOptions returns a ListOption object based on labels
func getListOptions(labels []string) metav1.ListOptions {
// toListOptions converts "key=value"-labels to Kubernetes LabelSelector
func toListOptions(labels []string) metav1.ListOptions {
labelSelector := fmt.Sprintf(strings.Join(labels, ","))
return metav1.ListOptions{
LabelSelector: labelSelector,
}
}

func missingLabelSelectorError(namespace, resource string) error {
return fmt.Errorf("label selector with --label expected. You can print out available labels with \"kubectl -n %s get %s --show-labels\"", namespace, resource)
}

func missingImageNameError(namespace string) error {
return fmt.Errorf("no image name given. On OpenShift, you can print available image streams with \"oc -n %s get imagestreams\"", namespace)
}
34 changes: 17 additions & 17 deletions cmd/configmaps.go
Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@ This command deletes ConfigMaps that are not being used anymore.`
)

var (
// configMapCmd represents a cobra command to clean up unused ConfigMaps
configMapCmd = &cobra.Command{
Use: "configmaps",
Short: "Cleans up your unused ConfigMaps in the Kubernetes cluster",
@@ -57,44 +56,45 @@ func init() {
}

func validateConfigMapCommandInput() error {

if len(config.Resource.Labels) == 0 {
return missingLabelSelectorError(config.Namespace, "configmaps")
}
if _, err := parseCutOffDateTime(config.Resource.OlderThan); err != nil {
return fmt.Errorf("Could not parse older-than flag: %w", err)
return fmt.Errorf("could not parse older-than flag: %w", err)
}
return nil
}

func executeConfigMapCleanupCommand(service configmap.Service) error {
c := config.Resource
namespace := config.Namespace
if len(config.Resource.Labels) == 0 {
err := service.PrintNamesAndLabels(namespace)
if err != nil {
return err
}
return nil
}

log.WithField("namespace", namespace).Debug("Looking for ConfigMaps")

foundConfigMaps, err := service.List(getListOptions(c.Labels))
log.WithField("namespace", namespace).Debug("Getting ConfigMaps")
foundConfigMaps, err := service.List(toListOptions(c.Labels))
if err != nil {
return fmt.Errorf("Could not retrieve config maps with labels '%s' for '%s': %w", c.Labels, namespace, err)
return fmt.Errorf("could not retrieve ConfigMaps with labels '%s' for '%s': %w", c.Labels, namespace, err)
}

unusedConfigMaps, err := service.GetUnused(namespace, foundConfigMaps)
if err != nil {
return fmt.Errorf("Could not retrieve unused config maps for '%s': %w", namespace, err)
return fmt.Errorf("could not retrieve unused config maps for '%s': %w", namespace, err)
}

cutOffDateTime, _ := parseCutOffDateTime(c.OlderThan)
filteredConfigMaps := service.FilterByTime(unusedConfigMaps, cutOffDateTime)
filteredConfigMaps = service.FilterByMaxCount(filteredConfigMaps, config.History.Keep)

if config.Delete {
service.Delete(filteredConfigMaps)
err := service.Delete(filteredConfigMaps)
if err != nil {
return fmt.Errorf("could not delete ConfigMaps for '%s': %s", namespace, err)
}
} else {
log.Infof("Showing results for --keep=%d and --older-than=%s", config.History.Keep, c.OlderThan)
log.WithFields(log.Fields{
"namespace": namespace,
"keep": config.History.Keep,
"older_than": c.OlderThan,
}).Info("Showing results")
service.Print(filteredConfigMaps)
}

5 changes: 1 addition & 4 deletions cmd/history.go
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ func init() {

func validateHistoryCommandInput(args []string) error {
if len(args) == 0 {
return nil
return missingImageNameError(config.Namespace)
}
if _, _, err := splitNamespaceAndImagestream(args[0]); err != nil {
return fmt.Errorf("could not parse image name: %w", err)
@@ -54,9 +54,6 @@ func validateHistoryCommandInput(args []string) error {

// ExecuteHistoryCleanupCommand executes the history cleanup command
func ExecuteHistoryCleanupCommand(args []string) error {
if len(args) == 0 {
return listImages()
}
c := config.History
namespace, imageName, _ := splitNamespaceAndImagestream(args[0])

25 changes: 25 additions & 0 deletions cmd/images.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cmd

import (
"errors"
"github.com/spf13/cobra"
"strings"
)

// imagesCmd represents the images command
@@ -14,3 +16,26 @@ var imagesCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(imagesCmd)
}

func splitNamespaceAndImagestream(repo string) (namespace string, image string, err error) {
if !strings.Contains(repo, "/") {
namespace = config.Namespace
image = repo
} else {
paths := strings.SplitAfter(repo, "/")
if len(paths) >= 3 {
namespace = paths[1]
image = paths[2]
} else {
namespace = paths[0]
image = paths[1]
}
}
if namespace == "" {
return "", "", errors.New("missing or invalid namespace")
}
if image == "" {
return "", "", errors.New("missing or invalid image name")
}
return strings.TrimSuffix(namespace, "/"), image, nil
}
30 changes: 1 addition & 29 deletions cmd/orphans.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package cmd

import (
"errors"
"fmt"
"regexp"
"strings"
"time"

"github.com/appuio/seiso/cfg"
@@ -56,7 +54,7 @@ func init() {

func validateOrphanCommandInput(args []string) error {
if len(args) == 0 {
return nil
return missingImageNameError(config.Namespace)
}
c := config.Orphan
if _, _, err := splitNamespaceAndImagestream(args[0]); err != nil {
@@ -78,9 +76,6 @@ func validateOrphanCommandInput(args []string) error {

// ExecuteOrphanCleanupCommand executes the orphan cleanup command
func ExecuteOrphanCleanupCommand(args []string) error {
if len(args) == 0 {
return listImages()
}
c := config.Orphan
namespace, imageName, _ := splitNamespaceAndImagestream(args[0])

@@ -140,26 +135,3 @@ func parseCutOffDateTime(olderThan string) (time.Time, error) {
}
return cutOffDateTime, nil
}

func splitNamespaceAndImagestream(repo string) (namespace string, image string, err error) {
if !strings.Contains(repo, "/") {
namespace = config.Namespace
image = repo
} else {
paths := strings.SplitAfter(repo, "/")
if len(paths) >= 3 {
namespace = paths[1]
image = paths[2]
} else {
namespace = paths[0]
image = paths[1]
}
}
if namespace == "" {
return "", "", errors.New("missing or invalid namespace")
}
if image == "" {
return "", "", errors.New("missing or invalid image name")
}
return strings.TrimSuffix(namespace, "/"), image, nil
}
8 changes: 8 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -72,6 +72,14 @@ func parseConfig(cmd *cobra.Command, args []string) {
} else {
log.SetLevel(level)
}
log.WithFields(log.Fields{
"namespace": config.Namespace,
"git": config.Git,
"log": config.Log,
"history": config.History,
"orphan": config.Orphan,
"resource": config.Resource,
}).Debug("Using config")
}

func loadEnvironmentVariables() {
36 changes: 17 additions & 19 deletions cmd/secrets.go
Original file line number Diff line number Diff line change
@@ -15,10 +15,9 @@ This command deletes secrets that are not being used anymore.`
)

var (
// secretCmd represents a cobra command to clean up unused secrets
secretCmd = &cobra.Command{
Use: "secrets",
Short: "Cleans up your unused secrets in the Kubernetes cluster",
Short: "Cleans up your unused Secrets in the Kubernetes cluster",
Long: secretCommandLongDescription,
Aliases: []string{"secret"},
Args: cobra.MaximumNArgs(1),
@@ -31,7 +30,7 @@ var (

coreClient, err := kubernetes.NewCoreV1Client()
if err != nil {
return fmt.Errorf("cannot initiate kubernetes core client")
return fmt.Errorf("cannot initiate kubernetes client")
}

secretService := secret.NewSecretsService(
@@ -48,41 +47,36 @@ func init() {
defaults := cfg.NewDefaultConfig()

secretCmd.PersistentFlags().BoolP("delete", "d", defaults.Delete, "Effectively delete Secrets found")
secretCmd.PersistentFlags().StringSliceP("label", "l", defaults.Resource.Labels, "Identify the Secret by these labels")
secretCmd.PersistentFlags().StringSliceP("label", "l", defaults.Resource.Labels, "Identify the Secrets by these labels")
secretCmd.PersistentFlags().IntP("keep", "k", defaults.History.Keep,
"Keep most current <k> Secrets; does not include currently used secret (if detected)")
secretCmd.PersistentFlags().String("older-than", defaults.Resource.OlderThan,
"Delete Secrets that are older than the duration, e.g. [1y2mo3w4d5h6m7s]")
}

func validateSecretCommandInput() error {
if len(config.Resource.Labels) == 0 {
return missingLabelSelectorError(config.Namespace, "secrets")
}
if _, err := parseCutOffDateTime(config.Resource.OlderThan); err != nil {
return fmt.Errorf("Could not parse older-than flag: %w", err)
return fmt.Errorf("could not parse older-than flag: %w", err)
}
return nil
}

func executeSecretCleanupCommand(service secret.Service) error {
c := config.Resource
namespace := config.Namespace
if len(config.Resource.Labels) == 0 {
err := service.PrintNamesAndLabels(namespace)
if err != nil {
return err
}
return nil
}

log.WithField("namespace", namespace).Debug("Looking for secrets")

foundSecrets, err := service.List(getListOptions(c.Labels))
log.WithField("namespace", namespace).Debug("Getting Secrets")
foundSecrets, err := service.List(toListOptions(c.Labels))
if err != nil {
return fmt.Errorf("could not retrieve secrets with labels '%s' for '%s': %w", c.Labels, namespace, err)
return fmt.Errorf("could not retrieve Secrets with labels '%s' for '%s': %w", c.Labels, namespace, err)
}

unusedSecrets, err := service.GetUnused(namespace, foundSecrets)
if err != nil {
return fmt.Errorf("could not retrieve unused secrets for '%s': %w", namespace, err)
return fmt.Errorf("could not retrieve unused Secrets for '%s': %w", namespace, err)
}

cutOffDateTime, _ := parseCutOffDateTime(c.OlderThan)
@@ -93,10 +87,14 @@ func executeSecretCleanupCommand(service secret.Service) error {
if config.Delete {
err := service.Delete(filteredSecrets)
if err != nil {
return fmt.Errorf("could not delete secrets for '%s': %s", namespace, err)
return fmt.Errorf("could not delete Secrets for '%s': %s", namespace, err)
}
} else {
log.Infof("Showing results for --keep=%d and --older-than=%s", config.History.Keep, c.OlderThan)
log.WithFields(log.Fields{
"namespace": namespace,
"keep": config.History.Keep,
"older_than": c.OlderThan,
}).Info("Showing results")
service.Print(filteredSecrets)
}

15 changes: 0 additions & 15 deletions pkg/configmap/configmap.go
Original file line number Diff line number Diff line change
@@ -17,8 +17,6 @@ import (

type (
Service interface {
// PrintNamesAndLabels return names and labels of ConfigMaps
PrintNamesAndLabels(namespace string) error
// List returns a list of ConfigMaps from a namespace
List(listOptions metav1.ListOptions) (configMaps []v1.ConfigMap, err error)
// GetUnused return unused ConfigMaps
@@ -53,24 +51,11 @@ func NewConfigMapsService(client core.ConfigMapInterface, helper kubernetes.Kube
}
}

func (cms ConfigMapsService) PrintNamesAndLabels(namespace string) error {
configMaps, err := cms.List(metav1.ListOptions{})
if err != nil {
return err
}
log.Infof("Following Config Maps are available in namespace %s", namespace)
for _, cm := range configMaps {
log.Infof("Name: %s, labels: %s", cm.Name, util.FlattenStringMap(cm.Labels))
}
return nil
}

func (cms ConfigMapsService) List(listOptions metav1.ListOptions) ([]v1.ConfigMap, error) {
configMaps, err := cms.client.List(listOptions)
if err != nil {
return nil, err
}

return configMaps.Items, nil
}

38 changes: 3 additions & 35 deletions pkg/configmap/configmap_test.go
Original file line number Diff line number Diff line change
@@ -30,37 +30,6 @@ func (k *HelperKubernetesErr) ResourceContains(namespace, value string, resource

var testNamespace = "testNamespace"

func Test_PrintNamesAndLabels(t *testing.T) {

tests := []struct {
name string
configMaps []v1.ConfigMap
expectErr bool
reaction test.ReactionFunc
}{
{
name: "GivenListOfConfigMaps_WhenListError_ThenReturnError",
configMaps: []v1.ConfigMap{},
expectErr: true,
reaction: createErrorReactor(),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
clientset := fake.NewSimpleClientset(convertToRuntime(tt.configMaps)[:]...)
clientset.PrependReactor("list", "configmaps", tt.reaction)
fakeClient := clientset.CoreV1().ConfigMaps(testNamespace)
service := NewConfigMapsService(fakeClient, &HelperKubernetes{}, ServiceConfiguration{})
err := service.PrintNamesAndLabels(testNamespace)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func Test_List(t *testing.T) {

tests := []struct {
@@ -131,7 +100,7 @@ func Test_FilterByTime(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := fake.NewSimpleClientset(&tt.configMaps[0], &tt.configMaps[1]).CoreV1().ConfigMaps(testNamespace)
fakeClient := fake.NewSimpleClientset(convertToRuntime(tt.configMaps)[:]...).CoreV1().ConfigMaps(testNamespace)
service := NewConfigMapsService(fakeClient, &HelperKubernetes{}, ServiceConfiguration{Batch: false})
filteredConfigMaps := service.FilterByTime(tt.configMaps, tt.cutOffDate)
assert.ElementsMatch(t, filteredConfigMaps, tt.expectedResult)
@@ -146,7 +115,6 @@ func Test_FilterByMaxCount(t *testing.T) {
configMaps []v1.ConfigMap
filteredConfigMaps []v1.ConfigMap
keep int
err error
}{
{
name: "GivenListOfConfigMaps_FilterByMaxCountOne_ThenReturnOneConfigMap",
@@ -171,8 +139,8 @@ func Test_FilterByMaxCount(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeConfigMapInterface := fake.NewSimpleClientset(&tt.configMaps[0], &tt.configMaps[1]).CoreV1().ConfigMaps(testNamespace)
service := NewConfigMapsService(fakeConfigMapInterface, &HelperKubernetes{}, ServiceConfiguration{Batch: false})
fakeConfigMapInterface := fake.NewSimpleClientset(convertToRuntime(tt.configMaps)[:]...).CoreV1().ConfigMaps(testNamespace)
service := NewConfigMapsService(fakeConfigMapInterface, &HelperKubernetes{}, ServiceConfiguration{})
filteredConfigMaps := service.FilterByMaxCount(tt.configMaps, tt.keep)
assert.ElementsMatch(t, filteredConfigMaps, tt.filteredConfigMaps)
})
14 changes: 0 additions & 14 deletions pkg/secret/secret.go
Original file line number Diff line number Diff line change
@@ -17,8 +17,6 @@ import (

type (
Service interface {
// PrintNamesAndLabels return the names and labels of all secrets
PrintNamesAndLabels(namespace string) error
// List returns a list of Secrets from a namespace
List(listOptions metav1.ListOptions) (resources []v1.Secret, err error)
// GetUnused returns unused Secrets.
@@ -54,18 +52,6 @@ func NewSecretsService(client core.SecretInterface, helper kubernetes.Kubernetes
}
}

func (ss SecretsService) PrintNamesAndLabels(namespace string) error {
secrets, err := ss.List(metav1.ListOptions{})
if err != nil {
return err
}
log.Infof("Following Secrets are available in namespace %s", namespace)
for _, s := range secrets {
log.Infof("Name: %s, labels: %s", s.Name, util.FlattenStringMap(s.Labels))
}
return nil
}

func (ss SecretsService) List(listOptions metav1.ListOptions) ([]v1.Secret, error) {
secrets, err := ss.client.List(listOptions)
if err != nil {
33 changes: 0 additions & 33 deletions pkg/secret/secret_test.go
Original file line number Diff line number Diff line change
@@ -31,39 +31,6 @@ func (k HelperKubernetesErr) ResourceContains(namespace, value string, resource

var testNamespace = "testNamespace"

func Test_PrintNamesAndLabels(t *testing.T) {

tests := []struct {
name string
secrets []v1.Secret
expectErr bool
reaction test.ReactionFunc
}{
{
name: "GivenListOfSecrets_WhenListError_ThenReturnError",
secrets: []v1.Secret{},
reaction: createErrorReactor(),
expectErr: true,
},
// TODO: Add test case that asserts for correct lines printed
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
clientset := fake.NewSimpleClientset(convertToRuntime(tt.secrets)[:]...)
clientset.PrependReactor("list", "secrets", tt.reaction)
fakeClient := clientset.CoreV1().Secrets(testNamespace)
service := NewSecretsService(fakeClient, &HelperKubernetes{}, ServiceConfiguration{})
err := service.PrintNamesAndLabels(testNamespace)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

func Test_List(t *testing.T) {

tests := []struct {
17 changes: 0 additions & 17 deletions pkg/util/common.go
Original file line number Diff line number Diff line change
@@ -2,26 +2,9 @@ package util

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sort"
"strings"
"time"
)

// FlattenStringMap turns a map of strings into a single string in the format of "[key1=value, key2=value]"
func FlattenStringMap(m map[string]string) string {
// Map keys are by design unordered, so we create an array of keys, sort them, then join together alphabetically.
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
pairs := make([]string, 0, len(m))
for _, k := range keys {
pairs = append(pairs, k + "=" + m[k])
}
return "[" + strings.Join(pairs, ", ") + "]"
}

// IsOlderThan returns true if the given resource is older than the specified timestamp. If the resource does not have
// a timestamp or is zero, it returns true.
func IsOlderThan(resource metav1.Object, olderThan time.Time) bool {
25 changes: 0 additions & 25 deletions pkg/util/common_test.go
Original file line number Diff line number Diff line change
@@ -8,31 +8,6 @@ import (
"time"
)

func TestFlattenStringMap(t *testing.T) {
tests := []struct {
name string
labels map[string]string
expected string
}{
{
name: "GivenStringMap_WhenSingleEntry_ThenReturnSingleString",
labels: map[string]string{"key": "value"},
expected: "[key=value]",
},
{
name: "GivenStringMap_WhenMultipleEntries_ThenReturnMultipleStringsWithinBrackets",
labels: map[string]string{"key1": "value", "key2": "value2"},
expected: "[key1=value, key2=value2]",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FlattenStringMap(tt.labels)
assert.Equal(t, tt.expected, result)
})
}
}

func TestCompareTimestamps(t *testing.T) {
tests := []struct {
name string

0 comments on commit 55aaafe

Please sign in to comment.