diff --git a/README.md b/README.md index 927e783..e71845a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Supported resources: - [x] ConfigMaps (not used in any Pods) - [x] Secrets (not used in any Pods or ServiceAccounts) - [x] Pods (whose status is not `Running`) +- [x] PersistentVolumeClaim (not used in any Pods) - [ ] PodDisruptionBudgets - [ ] HorizontalPodAutoscalers diff --git a/pkg/cmd/determiner.go b/pkg/cmd/determiner.go index 372ba2c..37c0b1d 100644 --- a/pkg/cmd/determiner.go +++ b/pkg/cmd/determiner.go @@ -5,30 +5,31 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" ) const ( - kindConfigMap = "ConfigMap" - kindSecret = "Secret" - kindPod = "Pod" + kindConfigMap = "ConfigMap" + kindSecret = "Secret" + kindPod = "Pod" + kindPersistentVolumeClaim = "PersistentVolumeClaim" ) // determiner determines whether a resource should be pruned. type determiner struct { - usedConfigMaps map[string]struct{} // key=ConfigMap.Name - usedSecrets map[string]struct{} // key=Secret.Name + usedConfigMaps map[string]struct{} // key=ConfigMap.Name + usedSecrets map[string]struct{} // key=Secret.Name + usedPersistentVolumeClaims map[string]struct{} // key=PersistentVolumeClaim.Name pods []*corev1.Pod } func newDeterminer(clientset *kubernetes.Clientset, r *resource.Result, namespace string) (*determiner, error) { var ( - pruneConfigMaps bool - pruneSecrets bool - prunePods bool + pruneConfigMaps bool + pruneSecrets bool + prunePersistentVolumeClaims bool ) if err := r.Visit(func(info *resource.Info, err error) error { @@ -37,8 +38,8 @@ func newDeterminer(clientset *kubernetes.Clientset, r *resource.Result, namespac pruneConfigMaps = true case kindSecret: pruneSecrets = true - case kindPod: - prunePods = true + case kindPersistentVolumeClaim: + prunePersistentVolumeClaims = true } return nil }); err != nil { @@ -49,7 +50,7 @@ func newDeterminer(clientset *kubernetes.Clientset, r *resource.Result, namespac ctx := context.Background() - if pruneConfigMaps || pruneSecrets || prunePods { + if pruneConfigMaps || pruneSecrets || prunePersistentVolumeClaims { var err error d.pods, err = listPods(ctx, clientset, namespace) if err != nil { @@ -69,6 +70,10 @@ func newDeterminer(clientset *kubernetes.Clientset, r *resource.Result, namespac d.usedSecrets = detectUsedSecrets(d.pods, sas) } + if prunePersistentVolumeClaims { + d.usedPersistentVolumeClaims = detectUsedPersistentVolumeClaims(d.pods) + } + return d, nil } @@ -85,12 +90,14 @@ func (d *determiner) determinePrune(info *resource.Info) (bool, error) { return true, nil } + case kindPersistentVolumeClaim: + if _, ok := d.usedPersistentVolumeClaims[info.Name]; !ok { + return true, nil + } + case kindPod: - var pod corev1.Pod - if err := runtime.DefaultUnstructuredConverter.FromUnstructured( - info.Object.(runtime.Unstructured).UnstructuredContent(), - &pod, - ); err != nil { + pod, err := infoToPod(info) + if err != nil { return false, err } @@ -184,3 +191,18 @@ func detectUsedSecrets(pods []*corev1.Pod, sas []*corev1.ServiceAccount) map[str return usedSecrets } + +func detectUsedPersistentVolumeClaims(pods []*corev1.Pod) map[string]struct{} { + usedPersistentVolumeClaims := make(map[string]struct{}) + + for _, pod := range pods { + for _, volume := range pod.Spec.Volumes { + if volume.PersistentVolumeClaim == nil { + continue + } + usedPersistentVolumeClaims[volume.PersistentVolumeClaim.ClaimName] = struct{}{} + } + } + + return usedPersistentVolumeClaims +} diff --git a/pkg/cmd/determiner_test.go b/pkg/cmd/determiner_test.go index 975e173..34d294e 100644 --- a/pkg/cmd/determiner_test.go +++ b/pkg/cmd/determiner_test.go @@ -9,10 +9,17 @@ import ( ) func Test_determiner_determinePrune(t *testing.T) { + const ( + fakeConfigMap = "fake-cm" + fakeSecret = "fake-secret" + fakePersistentVolumeClaim = "fake-pvc" + ) + type fields struct { - usedConfigMaps map[string]struct{} - usedSecrets map[string]struct{} - pods []*corev1.Pod + usedConfigMaps map[string]struct{} + usedSecrets map[string]struct{} + usedPersistentVolumes map[string]struct{} + pods []*corev1.Pod } type args struct { info *resource.Info @@ -29,7 +36,7 @@ func Test_determiner_determinePrune(t *testing.T) { name: "configmap should be pruned when it is used", fields: fields{ usedConfigMaps: map[string]struct{}{ - "fake-cm": {}, + fakeConfigMap: {}, }, pods: []*corev1.Pod{ { @@ -40,7 +47,7 @@ func Test_determiner_determinePrune(t *testing.T) { { ConfigMapRef: &corev1.ConfigMapEnvSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: "fake-cm", + Name: fakeConfigMap, }, }, }, @@ -58,7 +65,7 @@ func Test_determiner_determinePrune(t *testing.T) { Kind: kindConfigMap, }, }, - Name: "fake-cm", + Name: fakeConfigMap, }, }, want: false, @@ -73,7 +80,7 @@ func Test_determiner_determinePrune(t *testing.T) { Kind: kindConfigMap, }, }, - Name: "fake-cm", + Name: fakeConfigMap, }, }, want: true, @@ -83,7 +90,7 @@ func Test_determiner_determinePrune(t *testing.T) { name: "secret should be pruned when it is used", fields: fields{ usedSecrets: map[string]struct{}{ - "fake-secret": {}, + fakeSecret: {}, }, pods: []*corev1.Pod{ { @@ -94,7 +101,7 @@ func Test_determiner_determinePrune(t *testing.T) { { SecretRef: &corev1.SecretEnvSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: "fake-secret", + Name: fakeSecret, }, }, }, @@ -112,7 +119,7 @@ func Test_determiner_determinePrune(t *testing.T) { Kind: kindSecret, }, }, - Name: "fake-secret", + Name: fakeSecret, }, }, want: false, @@ -127,7 +134,57 @@ func Test_determiner_determinePrune(t *testing.T) { Kind: kindSecret, }, }, - Name: "fake-secret", + Name: fakeSecret, + }, + }, + want: true, + wantErr: false, + }, + { + name: "pvc should be pruned when it is used", + fields: fields{ + usedPersistentVolumes: map[string]struct{}{ + fakePersistentVolumeClaim: {}, + }, + pods: []*corev1.Pod{ + { + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: fakePersistentVolumeClaim, + }, + }, + }, + }, + }, + }, + }, + }, + args: args{ + info: &resource.Info{ + Object: &corev1.PersistentVolume{ + TypeMeta: metav1.TypeMeta{ + Kind: kindPersistentVolumeClaim, + }, + }, + Name: fakePersistentVolumeClaim, + }, + }, + want: false, + wantErr: false, + }, + { + name: "pvc should not be pruned when it is not used", + args: args{ + info: &resource.Info{ + Object: &corev1.PersistentVolume{ + TypeMeta: metav1.TypeMeta{ + Kind: kindPersistentVolumeClaim, + }, + }, + Name: fakePersistentVolumeClaim, }, }, want: true, @@ -142,9 +199,10 @@ func Test_determiner_determinePrune(t *testing.T) { t.Parallel() d := &determiner{ - usedConfigMaps: tt.fields.usedConfigMaps, - usedSecrets: tt.fields.usedSecrets, - pods: tt.fields.pods, + usedConfigMaps: tt.fields.usedConfigMaps, + usedSecrets: tt.fields.usedSecrets, + usedPersistentVolumeClaims: tt.fields.usedPersistentVolumes, + pods: tt.fields.pods, } got, err := d.determinePrune(tt.args.info) diff --git a/pkg/cmd/resource.go b/pkg/cmd/resource.go index 40165b9..c047dfb 100644 --- a/pkg/cmd/resource.go +++ b/pkg/cmd/resource.go @@ -5,6 +5,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" ) @@ -38,3 +40,15 @@ func podListToPods(podList *corev1.PodList) []*corev1.Pod { } return pods } + +func infoToPod(info *resource.Info) (*corev1.Pod, error) { + var pod corev1.Pod + if err := runtime.DefaultUnstructuredConverter.FromUnstructured( + info.Object.(runtime.Unstructured).UnstructuredContent(), + &pod, + ); err != nil { + return nil, err + } + + return &pod, nil +}