Skip to content

Commit

Permalink
Add ConfigMap and Secret cleaning
Browse files Browse the repository at this point in the history
Resolves: VT-1337
  • Loading branch information
zugao committed Apr 23, 2020
1 parent 965c05a commit c117fb2
Show file tree
Hide file tree
Showing 13 changed files with 649 additions and 14 deletions.
76 changes: 74 additions & 2 deletions README.md
Expand Up @@ -20,6 +20,15 @@ Inspired by Robert C. Martin's book, [Clean Code](https://www.investigatii.md/up
* Unused images in your container registry (identified by Image Stream Tags
in an [Image Stream](https://blog.openshift.com/image-streams-faq/) in OpenShift)

* Superflous [ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#understanding-configmaps-and-pods)s
in your Kubernetes cluster (e.g. generated by the Kustomize [configMapGenerator](
https://kubectl.docs.kubernetes.io/pages/reference/kustomize.html#configmapgenerator))

* Superflous [Secret](https://kubernetes.io/docs/concepts/configuration/secret/)
objects in your Kubernetes cluster (e.g. generated by the Kustomize [secretGenerator](
https://kubectl.docs.kubernetes.io/pages/reference/kustomize.html#secretgenerator))


## Why should I use this tool?

*Seiso* is intended to be used in application lifecycle management on application
Expand All @@ -30,6 +39,12 @@ resources that allow sophisticated, well-designed deployment processes.
For example, you may want to tag every application container image you build
with the [Git commit SHA-1 hash](https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection)
corresponding to the revision in source control that it was built from.
You may use [Kustomize](https://kustomize.io/) to generate `ConfigMap` and
`Secret` objects for your Kubernetes deployment using the [configMapGenerator or
secretGenerator](https://kubectl.docs.kubernetes.io/pages/reference/kustomize.html)
feature, which creates a history of rolled out configurations that provides you
with both a configuration audit trail and a safe way to roll back application
states.

While this is all convenient, those features were designed to create resources
but not to clean them up again. As a result, those resources will pile up, incur
Expand All @@ -51,6 +66,12 @@ The ImageStream "application" is invalid: []: Internal error: ImageStream.image.
1. It can be used more aggressively by deleting dangling image tags, "orphans",
that happen to exist when the Git history is altered (e.g. by force-pushing).

1. It can identify `ConfigMap` resources by its `config` label that are sufficiently
old to be deleted.

1. It can identify `Secret` resources by its `secret` label that are sufficiently
old to be deleted.

*Seiso* is opinionated, e.g. with respect to naming conventions of image tags,
either by relying on a long Git SHA-1 value (`namespace/app:a3d0df2c5060b87650df6a94a0a9600510303003`)
or a Git tag following semantic versioning (`namespace/app:v1.2.3`).
Expand All @@ -67,9 +88,9 @@ deletions during verifications or test runs.
* **Please watch out for shallow clones**, as the Git history might be missing,
it would in some cases also undesirably delete image tags.

## Usage
## Usage Imagestream

The following examples assume the namespace `namespace`, and and image stream
The following examples assume the namespace `namespace`, and image stream
named `app`. For the image cleanup to work, you need to be logged in to the target
cluster, as the tool will indirectly read your kubeconfig file.

Expand Down Expand Up @@ -133,6 +154,57 @@ This would delete `v1.9.3` as expected, since the `--sort` flag is `version` by
If `alphabetic`, the order for semver tags is reversed (probably undesired). For date-based tags, `alphabetic` sorting
flag might be better suitable, e.g. `2020-03-17`.

## Usage ConfigMaps and Secrets

The following examples assume the namespace `namespace`. For the seiso to work, you need to be logged in to the target cluster, as the tool will indirectly read your kubeconfig file.

For below examples, we will assume the following ConfigMaps and Secrets defined
in the cluster.

```
ConfigMaps:
- Name: C1
Labels: app=example,env=production
Used: yes
Age: 1m
- Name: C2
Labels: app=example,env=staging
Used: no
Age: 2m
- Name: C3
Labels: app=example
Used: no
Age: 1d
- Name: C4
Labels: app=example
Used: no
Age: 1w
Secrets
- Name: S1
Labels: app=example,env=prod,config=default
Used: no
Age: 1w
- Name: S2
Labels: app=example,env=staging,config=default
Used: no
Age: 1w
```

### Example: Delete unused ConfigMaps

```console
seiso configmaps namespace -l app=example --keep 1 --older-than=1d
```
This would delete unused ConfigMaps older than 5 hours, from the second element (sorted in descending order) and that have label `app=example`, more precisely `C4`.

### Example: Delete unused Secrets

```console
seiso secrets namespace -l app=example -l config=default --keep 0 --older-than=2w
```
This would delete secrets older than 2 weeks with labels `app=example` and `config=default`, more precisely `S1 and S2`.

## Migrate from legacy cleanup plugin

Projects using the legacy `oc` cleanup plugin can be migrated to `seiso` as follows
Expand Down
67 changes: 67 additions & 0 deletions cfg/resource.go
@@ -0,0 +1,67 @@
package cfg

import (
"time"
)

//KubernetesResource kubernetes resource interface
type KubernetesResource struct {
name string
namespace string
kind string
creationTimestamp time.Time
labels map[string]string
}

//GetName returns name of the resource
func (res *KubernetesResource) GetName() string { return res.name }

//SetName sets name of the resource
func (res *KubernetesResource) SetName(name string) { res.name = name }

//GetNamespace returns namespace of the resource
func (res *KubernetesResource) GetNamespace() string { return res.namespace }

//SetNamespace sets namespace of the resource
func (res *KubernetesResource) SetNamespace(namespace string) { res.namespace = namespace }

//GetKind returns type of the resource
func (res *KubernetesResource) GetKind() string { return res.kind }

//SetKind sets type of the resource
func (res *KubernetesResource) SetKind(kind string) { res.kind = kind }

//GetCreationTimestamp returns creation date of the resource
func (res *KubernetesResource) GetCreationTimestamp() time.Time { return res.creationTimestamp }

//SetCreationTimestamp sets creation date of the resource
func (res *KubernetesResource) SetCreationTimestamp(creationTimestamp time.Time) {
res.creationTimestamp = creationTimestamp
}

//GetLabels returns labels of the resource
func (res *KubernetesResource) GetLabels() map[string]string { return res.labels }

//SetLabels sets labels of the resource
func (res *KubernetesResource) SetLabels(labels map[string]string) { res.labels = labels }

//NewResource resource constructor
func NewResource(name, namespace, kind string, creationTimestamp time.Time, labels map[string]string) KubernetesResource {
return KubernetesResource{
name: name,
namespace: namespace,
kind: kind,
creationTimestamp: creationTimestamp,
labels: labels,
}
}

//NewConfigMapResource config map resource constructor
func NewConfigMapResource(name, namespace string, creationTimestamp time.Time, labels map[string]string) KubernetesResource {
return NewResource(name, namespace, "ConfigMap", creationTimestamp, labels)
}

//NewSecretResource secret resource constructor
func NewSecretResource(name, namespace string, creationTimestamp time.Time, labels map[string]string) KubernetesResource {
return NewResource(name, namespace, "Secret", creationTimestamp, labels)
}
34 changes: 29 additions & 5 deletions cfg/types.go
@@ -1,13 +1,19 @@
package cfg

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
core "k8s.io/client-go/kubernetes/typed/core/v1"
)

type (
// Configuration holds a strongly-typed tree of the configuration
Configuration struct {
Git GitConfig `mapstructure:",squash"`
History HistoryConfig `mapstructure:",squash"`
Orphan OrphanConfig `mapstructure:",squash"`
Log LogConfig
Force bool
Git GitConfig `mapstructure:",squash"`
History HistoryConfig `mapstructure:",squash"`
Orphan OrphanConfig `mapstructure:",squash"`
Resource ResourceConfig `mapstructure:",squash"`
Log LogConfig
Force bool
}
// GitConfig configures git repository
GitConfig struct {
Expand All @@ -25,11 +31,17 @@ type (
OlderThan string `mapstructure:"older-than"`
OrphanDeletionRegex string `mapstructure:"deletion-pattern"`
}
// LogConfig configures the log
LogConfig struct {
LogLevel string
Batch bool
Verbose bool
}
// ResourceConfig configures the resources and secrets
ResourceConfig struct {
Labels []string `mapstructure:"label"`
OlderThan string `mapstructure:"older-than"`
}
)

// NewDefaultConfig retrieves the hardcoded configs with sane defaults
Expand All @@ -48,6 +60,10 @@ func NewDefaultConfig() *Configuration {
OlderThan: "2mo",
OrphanDeletionRegex: "^[a-z0-9]{40}$",
},
Resource: ResourceConfig{
Labels: []string{},
OlderThan: "2mo",
},
Force: false,
Log: LogConfig{
LogLevel: "info",
Expand All @@ -56,3 +72,11 @@ func NewDefaultConfig() *Configuration {
},
}
}

//CoreObjectInterface defines interface for core kubernetes resources
type CoreObjectInterface interface {
Delete(name string, options *metav1.DeleteOptions) error
}

//ResourceNamespaceSelector gets resource from client
type ResourceNamespaceSelector func(*core.CoreV1Client) CoreObjectInterface
115 changes: 115 additions & 0 deletions cmd/common.go
Expand Up @@ -2,13 +2,16 @@ package cmd

import (
"fmt"
"strings"

"github.com/appuio/seiso/cfg"
"github.com/appuio/seiso/pkg/git"
"github.com/appuio/seiso/pkg/kubernetes"
"github.com/appuio/seiso/pkg/openshift"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/thoas/go-funk"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// DeleteImages deletes a list of image tags
Expand All @@ -34,6 +37,30 @@ func DeleteImages(imageTags []string, imageName string, namespace string, force
}
}

// DeleteResources deletes a list of ConfigMaps or secrets
func DeleteResources(resources []cfg.KubernetesResource, force bool, resourceSelectorFunc cfg.ResourceNamespaceSelector) {
if !force {
log.Warn("Force mode not enabled, nothing will be deleted")
}
for _, resource := range resources {
kind := resource.GetKind()
name := resource.GetName()
logEvent := log.WithFields(log.Fields{
"namespace": resource.GetNamespace(),
kind: name,
})
if force {
if err := openshift.DeleteResource(name, resourceSelectorFunc); err == nil {
logEvent.Info("Deleted resource")
} else {
logEvent.WithError(err).Error("Could not delete resource")
}
} else {
logEvent.Info("Would delete resource")
}
}
}

// PrintImageTags prints the given image tags line by line. In batch mode, only the tag name is printed, otherwise default
// log with info level
func PrintImageTags(imageTags []string) {
Expand All @@ -48,6 +75,20 @@ func PrintImageTags(imageTags []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 []cfg.KubernetesResource) {
if config.Log.Batch {
for _, resource := range resources {
fmt.Println(resource.GetKind() + ": " + resource.GetName())
}
} else {
for _, resource := range resources {
log.WithField(resource.GetKind(), resource.GetName()).Info("Found resource candidate")
}
}
}

// addCommonFlagsForGit sets up the force 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) {
Expand Down Expand Up @@ -80,3 +121,77 @@ func listImages() error {
}).Info("Please select an image. The following images are available")
return nil
}

func listConfigMaps(args []string) error {
namespace, err := getNamespace(args)
if err != nil {
return err
}

configMaps, err := openshift.ListConfigMaps(namespace, metav1.ListOptions{})
if err != nil {
return err
}

configMapNames, labels := getNamesAndLabels(configMaps)

log.WithFields(log.Fields{
"\n - project": namespace,
"\n - 🔓 configMaps": configMapNames,
"\n - 🎫 labels": labels,
}).Info("Please use labels to select ConfigMaps. The following ConfigMaps and Labels are available:")
return nil
}

func listSecrets(args []string) error {
namespace, err := getNamespace(args)
if err != nil {
return err
}
secrets, err := openshift.ListSecrets(namespace, metav1.ListOptions{})
if err != nil {
return err
}

secretNames, labels := getNamesAndLabels(secrets)
log.WithFields(log.Fields{
"\n - project": namespace,
"\n - 🔐 secrets": secretNames,
"\n - 🎫 labels": labels,
}).Info("Please use labels to select Secrets. The following Secrets and Labels are available:")
return nil
}

func getNamesAndLabels(resources []cfg.KubernetesResource) (resourceNames, labels []string) {
for _, resource := range resources {
resourceNames = append(resourceNames, resource.GetName())
for key, element := range resource.GetLabels() {
label := key + "=" + element
if !funk.ContainsString(labels, label) {
labels = append(labels, label)
}
}
}

return resourceNames, labels
}

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

func getNamespace(args []string) (string, error) {
if len(args) == 0 {
namespace, err := kubernetes.Namespace()
if err != nil {
return "", err
}
return namespace, err
} else {
return args[0], nil
}
}

0 comments on commit c117fb2

Please sign in to comment.