Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move logic out of the Configuration container
Browse files Browse the repository at this point in the history
- It shouldn't be necessary to figure out the namespace if we just pass --version flag
- other small improvements

Signed-off-by: Chris <github.account@chrigel.net>
ccremer committed Aug 5, 2020
1 parent 8850dfa commit 5ccc01e
Showing 9 changed files with 183 additions and 154 deletions.
11 changes: 0 additions & 11 deletions cfg/types.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package cfg

import (
"github.com/appuio/seiso/pkg/kubernetes"
log "github.com/sirupsen/logrus"
)

type (
// Configuration holds a strongly-typed tree of the configuration
Configuration struct {
@@ -47,13 +42,7 @@ type (

// NewDefaultConfig retrieves the hardcoded configs with sane defaults
func NewDefaultConfig() *Configuration {
namespace, err := kubernetes.Namespace()
if err != nil {
log.Warning("Unable to determine default namespace. Falling back to: default")
namespace = "default"
}
return &Configuration{
Namespace: namespace,
Git: GitConfig{
CommitLimit: 0,
RepoPath: ".",
47 changes: 26 additions & 21 deletions cmd/configmaps.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import (
"github.com/appuio/seiso/pkg/kubernetes"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"strings"
)

const (
@@ -22,24 +23,8 @@ var (
Aliases: []string{"configmap", "cm"},
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
log.SetFormatter(&log.TextFormatter{DisableTimestamp: true})
if err := validateConfigMapCommandInput(); err != nil {
cmd.Usage()
return err
}

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

configMapService := configmap.NewConfigMapsService(
coreClient.ConfigMaps(config.Namespace),
kubernetes.New(),
configmap.ServiceConfiguration{Batch: config.Log.Batch})
return executeConfigMapCleanupCommand(configMapService)
},
PreRunE: validateConfigMapCommandInput,
RunE: executeConfigMapCleanupCommand,
}
)

@@ -48,26 +33,46 @@ func init() {
defaults := cfg.NewDefaultConfig()

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

func validateConfigMapCommandInput() error {
func validateConfigMapCommandInput(cmd *cobra.Command, args []string) (returnErr error) {
defer func() {
if returnErr != nil {
cmd.Usage()
}
}()
if len(config.Resource.Labels) == 0 {
return missingLabelSelectorError(config.Namespace, "configmaps")
}
for _, label := range config.Resource.Labels {
if !strings.Contains(label, "=") {
return fmt.Errorf("incorrect label format does not match expected \"key=value\" format: %s", label)
}
}
if _, err := parseCutOffDateTime(config.Resource.OlderThan); err != nil {
return fmt.Errorf("could not parse older-than flag: %w", err)
}
return nil
}

func executeConfigMapCleanupCommand(service configmap.Service) error {
func executeConfigMapCleanupCommand(cmd *cobra.Command, args []string) error {
coreClient, err := kubernetes.NewCoreV1Client()
if err != nil {
return fmt.Errorf("cannot initiate kubernetes client: %w", err)
}

c := config.Resource
namespace := config.Namespace
service := configmap.NewConfigMapsService(
coreClient.ConfigMaps(namespace),
kubernetes.New(),
configmap.ServiceConfiguration{Batch: config.Log.Batch})

log.WithField("namespace", namespace).Debug("Getting ConfigMaps")
foundConfigMaps, err := service.List(toListOptions(c.Labels))
27 changes: 16 additions & 11 deletions cmd/history.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@ package cmd

import (
"fmt"

"github.com/appuio/seiso/cfg"
"github.com/appuio/seiso/pkg/cleanup"
"github.com/appuio/seiso/pkg/git"
@@ -19,13 +18,8 @@ var (
Long: `Clean up excessive image tags matching the commit hashes (prefix) of the git repository`,
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateHistoryCommandInput(args); err != nil {
cmd.Usage()
return err
}
return ExecuteHistoryCleanupCommand(args)
},
PreRunE: validateHistoryCommandInput,
RunE: ExecuteHistoryCleanupCommand,
}
)

@@ -39,21 +33,32 @@ func init() {

}

func validateHistoryCommandInput(args []string) error {
func validateHistoryCommandInput(cmd *cobra.Command, args []string) (returnErr error) {
defer func() {
if returnErr != nil {
cmd.Usage()
}
}()
if len(args) == 0 {
return missingImageNameError(config.Namespace)
}
if _, _, err := splitNamespaceAndImagestream(args[0]); err != nil {
namespace, image, err := splitNamespaceAndImagestream(args[0])
if err != nil {
return fmt.Errorf("could not parse image name: %w", err)
}
if config.Git.Tag && !git.IsValidSortValue(config.Git.SortCriteria) {
return fmt.Errorf("invalid sort flag provided: %v", config.Git.SortCriteria)
}
log.WithFields(log.Fields{
"namespace": namespace,
"image": image,
}).Debug("Using image config")
config.Namespace = namespace
return nil
}

// ExecuteHistoryCleanupCommand executes the history cleanup command
func ExecuteHistoryCleanupCommand(args []string) error {
func ExecuteHistoryCleanupCommand(cmd *cobra.Command, args []string) error {
c := config.History
namespace, imageName, _ := splitNamespaceAndImagestream(args[0])

6 changes: 3 additions & 3 deletions cmd/images.go
Original file line number Diff line number Diff line change
@@ -24,10 +24,10 @@ func splitNamespaceAndImagestream(repo string) (namespace string, image string,
} else {
paths := strings.SplitAfter(repo, "/")
if len(paths) >= 3 {
namespace = paths[1]
namespace = strings.TrimSuffix(paths[1], "/")
image = paths[2]
} else {
namespace = paths[0]
namespace = strings.TrimSuffix(paths[0], "/")
image = paths[1]
}
}
@@ -37,5 +37,5 @@ func splitNamespaceAndImagestream(repo string) (namespace string, image string,
if image == "" {
return "", "", errors.New("missing or invalid image name")
}
return strings.TrimSuffix(namespace, "/"), image, nil
return namespace, image, nil
}
77 changes: 77 additions & 0 deletions cmd/images_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cmd

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_SplitNamespaceAndImagestream(t *testing.T) {
type args struct {
repo string
}
tests := []struct {
name string
args args
expectedNamespace string
expectedImage string
wantErr bool
}{
{
name: "ShouldSplit_NamespaceAndImageName",
args: args{
repo: "namespace/image",
},
expectedNamespace: "namespace",
expectedImage: "image",
},
{
name: "ShouldReturnActiveNamespace_IfRepoDoesNotContainNamespace",
args: args{
repo: "image",
},
expectedNamespace: "currently-active-ns",
expectedImage: "image",
},
{
name: "ShouldThrowError_IfRepoDoesNotContainImage",
args: args{
repo: "namespace/",
},
wantErr: true,
},
{
name: "ShouldThrowError_IfRepoIsInvalid",
args: args{
repo: "/",
},
wantErr: true,
},
{
name: "ShouldThrowError_IfRepoIsEmpty",
args: args{},
wantErr: true,
},
{
name: "ShouldIgnore_Registry",
args: args{
repo: "docker.io/namespace/image",
},
expectedNamespace: "namespace",
expectedImage: "image",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config.Namespace = "currently-active-ns"
namespace, image, err := splitNamespaceAndImagestream(tt.args.repo)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, namespace, tt.expectedNamespace)
assert.Equal(t, image, tt.expectedImage)
})
}
}
28 changes: 17 additions & 11 deletions cmd/orphans.go
Original file line number Diff line number Diff line change
@@ -31,13 +31,8 @@ var (
Aliases: []string{"orph", "orphan"},
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateOrphanCommandInput(args); err != nil {
cmd.Usage()
return err
}
return ExecuteOrphanCleanupCommand(args)
},
PreRunE: validateOrphanCommandInput,
RunE: ExecuteOrphanCleanupCommand,
}
)

@@ -52,13 +47,19 @@ func init() {
"Delete images that match the regex, defaults to matching Git SHA commits")
}

func validateOrphanCommandInput(args []string) error {
func validateOrphanCommandInput(cmd *cobra.Command, args []string) (returnErr error) {
defer func() {
if returnErr != nil {
cmd.Usage()
}
}()
if len(args) == 0 {
return missingImageNameError(config.Namespace)
}
c := config.Orphan
if _, _, err := splitNamespaceAndImagestream(args[0]); err != nil {
return err
namespace, image, err := splitNamespaceAndImagestream(args[0])
if err != nil {
return fmt.Errorf("could not parse image name: %w", err)
}
if _, err := parseOrphanDeletionRegex(c.OrphanDeletionRegex); err != nil {
return fmt.Errorf("could not parse orphan deletion pattern: %w", err)
@@ -71,11 +72,16 @@ func validateOrphanCommandInput(args []string) error {
if config.Git.Tag && !git.IsValidSortValue(config.Git.SortCriteria) {
return fmt.Errorf("invalid sort flag provided: %v", config.Git.SortCriteria)
}
log.WithFields(log.Fields{
"namespace": namespace,
"image": image,
}).Debug("Using image config")
config.Namespace = namespace
return nil
}

// ExecuteOrphanCleanupCommand executes the orphan cleanup command
func ExecuteOrphanCleanupCommand(args []string) error {
func ExecuteOrphanCleanupCommand(cmd *cobra.Command, args []string) error {
c := config.Orphan
namespace, imageName, _ := splitNamespaceAndImagestream(args[0])

73 changes: 2 additions & 71 deletions cmd/orphans_test.go
Original file line number Diff line number Diff line change
@@ -1,83 +1,14 @@
package cmd

import (
"github.com/spf13/cobra"
"regexp"
"testing"

"github.com/appuio/seiso/cfg"
"github.com/stretchr/testify/assert"
)

func Test_splitNamespaceAndImagestream(t *testing.T) {
type args struct {
repo string
}
tests := []struct {
name string
args args
expectedNamespace string
expectedImage string
wantErr bool
}{
{
name: "ShouldSplit_NamespaceAndImageName",
args: args{
repo: "namespace/image",
},
expectedNamespace: "namespace",
expectedImage: "image",
},
{
name: "ShouldReturnActiveNamespace_IfRepoDoesNotContainNamespace",
args: args{
repo: "image",
},
expectedNamespace: "currently-active-ns",
expectedImage: "image",
},
{
name: "ShouldThrowError_IfRepoDoesNotContainImage",
args: args{
repo: "namespace/",
},
wantErr: true,
},
{
name: "ShouldThrowError_IfRepoIsInvalid",
args: args{
repo: "/",
},
wantErr: true,
},
{
name: "ShouldThrowError_IfRepoIsEmpty",
args: args{},
wantErr: true,
},
{
name: "ShouldIgnore_Registry",
args: args{
repo: "docker.io/namespace/image",
},
expectedNamespace: "namespace",
expectedImage: "image",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config.Namespace = "currently-active-ns"
namespace, image, err := splitNamespaceAndImagestream(tt.args.repo)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, namespace, tt.expectedNamespace)
assert.Equal(t, image, tt.expectedImage)
})
}
}

func Test_parseOrphanDeletionRegex(t *testing.T) {
type args struct {
orphanIncludeRegex string
@@ -173,7 +104,7 @@ func Test_validateOrphanCommandInput(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config = &tt.input.config
err := validateOrphanCommandInput(tt.input.args)
err := validateOrphanCommandInput(&cobra.Command{}, tt.input.args)
if tt.wantErr {
assert.Error(t, err)
return
22 changes: 16 additions & 6 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"fmt"
"github.com/appuio/seiso/pkg/kubernetes"
"os"
"strings"

@@ -16,9 +18,9 @@ import (
var (
// rootCmd represents the base command when called without any subcommands
rootCmd = &cobra.Command{
Use: "seiso",
Short: "Keeps your Kubernetes projects clean",
PersistentPreRun: parseConfig,
Use: "seiso",
Short: "Keeps your Kubernetes projects clean",
PersistentPreRunE: parseConfig,
}
config = cfg.NewDefaultConfig()
koanfInstance = koanf.New(".")
@@ -32,7 +34,7 @@ func Execute() error {
func init() {
rootCmd.PersistentFlags().StringP("namespace", "n", config.Namespace, "Cluster namespace of current context")
rootCmd.PersistentFlags().String("log.level", config.Log.LogLevel, "Log level, one of [debug info warn error fatal]")
rootCmd.PersistentFlags().BoolP("log.verbose", "v", config.Log.Verbose, "Shorthand for --log.level debug")
rootCmd.PersistentFlags().BoolP("log.verbose", "v", config.Log.Verbose, "Shorthand for \"--log.level debug\"")
rootCmd.PersistentFlags().BoolP("log.batch", "b", config.Log.Batch,
"Use Batch mode (Prints error to StdErr, StdOut is used to just print resource names, useful for piping)")
cobra.OnInitialize(initRootConfig)
@@ -43,13 +45,13 @@ func initRootConfig() {
}

// parseConfig reads the flags and ENV vars
func parseConfig(cmd *cobra.Command, args []string) {
func parseConfig(cmd *cobra.Command, args []string) error {

loadEnvironmentVariables()
bindFlags(cmd.PersistentFlags())

if err := koanfInstance.Unmarshal("", &config); err != nil {
log.WithError(err).Fatal("Could not read config")
return fmt.Errorf("could not read config: %w", err)
}

log.SetFormatter(&log.TextFormatter{
@@ -72,6 +74,13 @@ func parseConfig(cmd *cobra.Command, args []string) {
} else {
log.SetLevel(level)
}
if config.Namespace == "" {
namespace, err := kubernetes.Namespace()
if err != nil {
return fmt.Errorf("unable to determine default namespace from Kubeconfig and --namespace not given: %w", err)
}
config.Namespace = namespace
}
log.WithFields(log.Fields{
"namespace": config.Namespace,
"git": config.Git,
@@ -80,6 +89,7 @@ func parseConfig(cmd *cobra.Command, args []string) {
"orphan": config.Orphan,
"resource": config.Resource,
}).Debug("Using config")
return nil
}

func loadEnvironmentVariables() {
46 changes: 26 additions & 20 deletions cmd/secrets.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import (
"github.com/appuio/seiso/pkg/secret"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"strings"
)

const (
@@ -22,23 +23,8 @@ var (
Aliases: []string{"secret"},
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateSecretCommandInput(); err != nil {
cmd.Usage()
return err
}

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

secretService := secret.NewSecretsService(
coreClient.Secrets(config.Namespace),
kubernetes.New(),
secret.ServiceConfiguration{Batch: config.Log.Batch})
return executeSecretCleanupCommand(secretService)
},
PreRunE: validateSecretCommandInput,
RunE: executeSecretCleanupCommand,
}
)

@@ -47,26 +33,46 @@ 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 Secrets by these labels")
secretCmd.PersistentFlags().StringSliceP("label", "l", defaults.Resource.Labels,
"Identify the Secrets by these \"key=value\" 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 {
func validateSecretCommandInput(cmd *cobra.Command, args []string) (returnErr error) {
defer func() {
if returnErr != nil {
cmd.Usage()
}
}()
if len(config.Resource.Labels) == 0 {
return missingLabelSelectorError(config.Namespace, "secrets")
}
for _, label := range config.Resource.Labels {
if !strings.Contains(label, "=") {
return fmt.Errorf("incorrect label format does not match expected \"key=value\" format: %s", label)
}
}
if _, err := parseCutOffDateTime(config.Resource.OlderThan); err != nil {
return fmt.Errorf("could not parse older-than flag: %w", err)
}
return nil
}

func executeSecretCleanupCommand(service secret.Service) error {
func executeSecretCleanupCommand(cmd *cobra.Command, args []string) error {
coreClient, err := kubernetes.NewCoreV1Client()
if err != nil {
return fmt.Errorf("cannot initiate kubernetes client: %w", err)
}

c := config.Resource
namespace := config.Namespace
service := secret.NewSecretsService(
coreClient.Secrets(namespace),
kubernetes.New(),
secret.ServiceConfiguration{Batch: config.Log.Batch})

log.WithField("namespace", namespace).Debug("Getting Secrets")
foundSecrets, err := service.List(toListOptions(c.Labels))

0 comments on commit 5ccc01e

Please sign in to comment.