Skip to content

Commit

Permalink
Merge pull request #53 from appuio/cleanup-ns
Browse files Browse the repository at this point in the history
Cleanup empty namespaces
  • Loading branch information
cimnine committed May 7, 2021
2 parents 9e05710 + f32ff85 commit dbf5fec
Show file tree
Hide file tree
Showing 21 changed files with 1,120 additions and 100 deletions.
29 changes: 0 additions & 29 deletions .github/workflows/build.yml

This file was deleted.

2 changes: 0 additions & 2 deletions .github/workflows/release.yml
Expand Up @@ -31,8 +31,6 @@ jobs:
run: docker login -u "${{ secrets.DOCKER_HUB_USER }}" -p "${{ secrets.DOCKER_HUB_PASSWORD }}"
- name: Login to quay.io
run: docker login -u "${{ secrets.QUAY_IO_USER }}" -p "${{ secrets.QUAY_IO_PASSWORD }}" quay.io
- name: Generate artifacts
run: make crd
- name: Build changelog from PRs with labels
id: build_changelog
uses: mikepenz/release-changelog-builder-action@v1
Expand Down
2 changes: 2 additions & 0 deletions .goreleaser.yml
Expand Up @@ -32,6 +32,8 @@ dockers:
- image_templates:
- "docker.io/appuio/seiso:v{{ .Version }}"
- "docker.io/appuio/seiso:v{{ .Major }}"
- "quay.io/appuio/seiso:v{{ .Version }}"
- "quay.io/appuio/seiso:v{{ .Major }}"

nfpms:
- vendor: APPUiO
Expand Down
6 changes: 5 additions & 1 deletion README.md
Expand Up @@ -27,6 +27,9 @@ Inspired by Robert C. Martin's book, [Clean Code](https://www.investigatii.md/up
objects in your Kubernetes cluster (e.g. generated by the Kustomize [secretGenerator](
https://kubectl.docs.kubernetes.io/pages/reference/kustomize.html#secretgenerator))

* Empty [Namespaces](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/)
in your Kubernetes cluster.

## Usage

```console
Expand All @@ -38,6 +41,7 @@ seiso image history --help
seiso image orphans --help
seiso configmaps --help
seiso secrets --help
seiso namespaces --help
```

## Why should I use this tool?
Expand Down Expand Up @@ -242,7 +246,7 @@ seiso -n "$OPENSHIFT_PROJECT" image orphans "$APP_NAME" --delete --older-than 1w

Requirements:

* go 1.14
* go
* goreleaser
* Docker

Expand Down
10 changes: 6 additions & 4 deletions cfg/types.go
Expand Up @@ -35,8 +35,9 @@ type (
}
// ResourceConfig configures the resources and secrets
ResourceConfig struct {
Labels []string `koanf:"label"`
OlderThan string `koanf:"older-than"`
Labels []string `koanf:"label"`
OlderThan string `koanf:"older-than"`
DeleteAfter string `koanf:"delete-after"`
}
)

Expand All @@ -57,8 +58,9 @@ func NewDefaultConfig() *Configuration {
OrphanDeletionRegex: "^[a-z0-9]{40}$",
},
Resource: ResourceConfig{
Labels: []string{},
OlderThan: "1w",
Labels: []string{},
OlderThan: "1w",
DeleteAfter: "24h",
},
Delete: false,
Log: LogConfig{
Expand Down
1 change: 0 additions & 1 deletion cmd/configmaps.go
Expand Up @@ -23,7 +23,6 @@ var (
Short: "Cleans up your unused ConfigMaps in the Kubernetes cluster",
Long: configMapCommandLongDescription,
Aliases: []string{"configmap", "cm"},
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
PreRunE: validateConfigMapCommandInput,
RunE: executeConfigMapCleanupCommand,
Expand Down
121 changes: 121 additions & 0 deletions cmd/namespace_test.go
@@ -0,0 +1,121 @@
package cmd

import (
"testing"

"github.com/spf13/cobra"

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

func Test_validateNamespaceCommandInput(t *testing.T) {
type args struct {
args []string
config cfg.Configuration
}
tests := map[string]struct {
name string
input args
wantErr bool
}{
"ShouldThrowError_IfNoLabelSelector": {
input: args{
config: cfg.Configuration{
Resource: cfg.ResourceConfig{
DeleteAfter: "1s",
},
},
},
wantErr: true,
},
"ShouldThrowError_InvalidLabelSelector": {
input: args{
config: cfg.Configuration{
Resource: cfg.ResourceConfig{
DeleteAfter: "1s",
Labels: []string{"invalid"},
},
},
},
wantErr: true,
},
"ShouldThrowError_IfInvalidDeleteAfterFlag": {
input: args{
config: cfg.Configuration{
Resource: cfg.ResourceConfig{
Labels: []string{"some=label"},
DeleteAfter: "invalid",
},
},
},
wantErr: true,
},
"ShouldThrowError_IfNegativeDeleteAfterFlag": {
input: args{
config: cfg.Configuration{
Resource: cfg.ResourceConfig{
Labels: []string{"some=label"},
DeleteAfter: "-1s",
},
},
},
wantErr: true,
},
"Success_IfValidDeleteAfterFlag1d": {
input: args{
config: cfg.Configuration{
Resource: cfg.ResourceConfig{
Labels: []string{"some=label"},
DeleteAfter: "1d",
},
},
},
wantErr: false,
},
"Success_IfValidDeleteAfterFlag1d1y": {
input: args{
config: cfg.Configuration{
Resource: cfg.ResourceConfig{
Labels: []string{"some=label"},
DeleteAfter: "1d1y",
},
},
},
wantErr: false,
},
"Success_IfValidDeleteAfterFlag1w1m": {
input: args{
config: cfg.Configuration{
Resource: cfg.ResourceConfig{
Labels: []string{"some=label"},
DeleteAfter: "1w1m",
},
},
},
wantErr: false,
},
"Success_IfValidDeleteAfterFlag1h1s": {
input: args{
config: cfg.Configuration{
Resource: cfg.ResourceConfig{
Labels: []string{"some=label"},
DeleteAfter: "1h1s",
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config = &tt.input.config
err := validateNsCommandInput(&cobra.Command{}, tt.input.args)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}
104 changes: 104 additions & 0 deletions cmd/namespaces.go
@@ -0,0 +1,104 @@
package cmd

import (
"context"
"fmt"
"strings"

"github.com/appuio/seiso/cfg"
"github.com/appuio/seiso/pkg/kubernetes"
"github.com/appuio/seiso/pkg/namespace"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

const (
nsCommandLongDescription = `Sometimes Namespaces are left empty in a Kubernetes cluster.
This command deletes Namespaces that are not being used anymore.
A Namespace is deemed empty if no Helm releases, Pods, Deployments, StatefulSets or DaemonSets can be found.`
)

var (
nsCmd = &cobra.Command{
Use: "namespaces",
Short: "Cleans up your empty Namespaces",
Long: nsCommandLongDescription,
Aliases: []string{"namespace", "ns"},
SilenceUsage: true,
PreRunE: validateNsCommandInput,
RunE: executeNsCleanupCommand,
}
)

func init() {
rootCmd.AddCommand(nsCmd)
defaults := cfg.NewDefaultConfig()

nsCmd.PersistentFlags().BoolP("delete", "d", defaults.Delete, "Effectively delete Namespaces found")
nsCmd.PersistentFlags().StringSliceP("label", "l", defaults.Resource.Labels,
"Identify the Namespaces by these \"key=value\" labels")
nsCmd.PersistentFlags().String("delete-after", defaults.Resource.DeleteAfter,
"Only delete Namespaces after they were empty for this duration, e.g. [1y2mo3w4d5h6m7s]")
}

func validateNsCommandInput(cmd *cobra.Command, _ []string) (returnErr error) {
defer showUsageOnError(cmd, returnErr)
if len(config.Resource.Labels) == 0 {
return missingLabelSelectorError(config.Namespace, "namespaces")
}
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.DeleteAfter); err != nil {
return fmt.Errorf("could not parse delete-after flag %w", err)
}
return nil
}

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

dynamicClient, err := kubernetes.NewDynamicClient()
if err != nil {
return fmt.Errorf("cannot initiate kubernetes dynamic client: %w", err)
}

ctx := context.Background()
c := config.Resource
service := namespace.NewNamespacesService(
coreClient.Namespaces(),
dynamicClient,
namespace.ServiceConfiguration{
Batch: config.Log.Batch,
})

log.Debug("Getting Namespaces")
allNamespaces, err := service.List(ctx, toListOptions(c.Labels))
if err != nil {
return fmt.Errorf("could not retrieve Namespaces with labels %q: %w", c.Labels, err)
}

emptyNamespaces, err := service.GetEmptyFor(ctx, allNamespaces, c.DeleteAfter)
if err != nil {
return fmt.Errorf("could not retrieve empty namespaces %w", err)
}

if config.Delete {
err := service.Delete(ctx, emptyNamespaces)
if err != nil {
return fmt.Errorf("could not delete Namespaces %w", err)
}
} else {
log.WithFields(log.Fields{
"delete_after": c.DeleteAfter,
}).Info("Showing results")
service.Print(emptyNamespaces)
}

return nil
}
7 changes: 4 additions & 3 deletions cmd/orphans.go
Expand Up @@ -10,7 +10,8 @@ import (
"github.com/appuio/seiso/pkg/cleanup"
"github.com/appuio/seiso/pkg/git"
"github.com/appuio/seiso/pkg/openshift"
"github.com/karrick/tparse"
"github.com/appuio/seiso/pkg/util"
"github.com/karrick/tparse/v2"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -78,7 +79,7 @@ func validateOrphanCommandInput(cmd *cobra.Command, args []string) (returnErr er
}

// ExecuteOrphanCleanupCommand executes the orphan cleanup command
func ExecuteOrphanCleanupCommand(cmd *cobra.Command, args []string) error {
func ExecuteOrphanCleanupCommand(_ *cobra.Command, args []string) error {
c := config.Orphan
ctx := context.Background()
namespace, imageName, _ := splitNamespaceAndImagestream(args[0])
Expand Down Expand Up @@ -133,7 +134,7 @@ func parseCutOffDateTime(olderThan string) (time.Time, error) {
if len(olderThan) == 0 {
return time.Now(), nil
}
cutOffDateTime, err := tparse.ParseNow(time.RFC3339, "now-"+olderThan)
cutOffDateTime, err := tparse.ParseNow(util.TimeFormat, "now-"+olderThan)
if err != nil {
return time.Now(), err
}
Expand Down
1 change: 0 additions & 1 deletion cmd/secrets.go
Expand Up @@ -23,7 +23,6 @@ var (
Short: "Cleans up your unused Secrets in the Kubernetes cluster",
Long: secretCommandLongDescription,
Aliases: []string{"secret"},
Args: cobra.MaximumNArgs(1),
SilenceUsage: true,
PreRunE: validateSecretCommandInput,
RunE: executeSecretCleanupCommand,
Expand Down

0 comments on commit dbf5fec

Please sign in to comment.