diff --git a/deploy/craned/deployment.yaml b/deploy/craned/deployment.yaml index be013e17e..f7913bbc3 100644 --- a/deploy/craned/deployment.yaml +++ b/deploy/craned/deployment.yaml @@ -119,6 +119,10 @@ data: acceptedResources: - kind: Node apiVersion: v1 + - name: Volumes + acceptedResources: + - kind: PersistentVolume + apiVersion: v1 --- apiVersion: v1 kind: ConfigMap diff --git a/pkg/recommendation/framework/context.go b/pkg/recommendation/framework/context.go index 0b53341e3..ab6b600c4 100644 --- a/pkg/recommendation/framework/context.go +++ b/pkg/recommendation/framework/context.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "sync" jsonpatch "github.com/evanphx/json-patch" @@ -178,6 +179,19 @@ func RetrieveScale(ctx *RecommendationContext) error { return nil } +func RetrieveVolumes(ctx *RecommendationContext) error { + if ctx.Recommendation.Spec.TargetRef.Kind == "PersistentVolume" { + volumes, err := utils.GetOrphanVolumes(ctx.Client) + if len(volumes) == 0 { + return err + } + str := strings.Join(volumes, ",") + ctx.Recommendation.Status.RecommendedValue = str + return err + } + return nil +} + func RetrievePods(ctx *RecommendationContext) error { if ctx.Recommendation.Spec.TargetRef.Kind == "Node" { pods, err := utils.GetNodePods(ctx.Client, ctx.Recommendation.Spec.TargetRef.Name) diff --git a/pkg/recommendation/recommender/const.go b/pkg/recommendation/recommender/const.go index 56d7ad097..180e27404 100644 --- a/pkg/recommendation/recommender/const.go +++ b/pkg/recommendation/recommender/const.go @@ -12,4 +12,7 @@ const ( // IdleNodeRecommender name IdleNodeRecommender string = "IdleNode" + + // VolumesRecommender name + VolumesRecommender string = "Volumes" ) diff --git a/pkg/recommendation/recommender/volumes/filter.go b/pkg/recommendation/recommender/volumes/filter.go new file mode 100644 index 000000000..087399bed --- /dev/null +++ b/pkg/recommendation/recommender/volumes/filter.go @@ -0,0 +1,21 @@ +package volumes + +import ( + "github.com/gocrane/crane/pkg/recommendation/framework" +) + +// Filter out k8s resources that are not supported by the recommender. +func (vr *VolumesRecommender) Filter(ctx *framework.RecommendationContext) error { + var err error + + // filter resource that not match objectIdentity + if err = vr.BaseRecommender.Filter(ctx); err != nil { + return err + } + + if err = framework.RetrieveVolumes(ctx); err != nil { + return err + } + + return nil +} diff --git a/pkg/recommendation/recommender/volumes/observe.go b/pkg/recommendation/recommender/volumes/observe.go new file mode 100644 index 000000000..ab48d714a --- /dev/null +++ b/pkg/recommendation/recommender/volumes/observe.go @@ -0,0 +1,10 @@ +package volumes + +import ( + "github.com/gocrane/crane/pkg/recommendation/framework" +) + +// Observe enhance the observability. +func (s *VolumesRecommender) Observe(ctx *framework.RecommendationContext) error { + return nil +} diff --git a/pkg/recommendation/recommender/volumes/prepare.go b/pkg/recommendation/recommender/volumes/prepare.go new file mode 100644 index 000000000..dc7227123 --- /dev/null +++ b/pkg/recommendation/recommender/volumes/prepare.go @@ -0,0 +1,22 @@ +package volumes + +import ( + "github.com/gocrane/crane/pkg/recommendation/framework" +) + +// CheckDataProviders in PrePrepare phase, will create data source provider via your recommendation config. +func (rr *VolumesRecommender) CheckDataProviders(ctx *framework.RecommendationContext) error { + if err := rr.BaseRecommender.CheckDataProviders(ctx); err != nil { + return err + } + + return nil +} + +func (rr *VolumesRecommender) CollectData(ctx *framework.RecommendationContext) error { + return nil +} + +func (rr *VolumesRecommender) PostProcessing(ctx *framework.RecommendationContext) error { + return nil +} diff --git a/pkg/recommendation/recommender/volumes/recommend.go b/pkg/recommendation/recommender/volumes/recommend.go new file mode 100644 index 000000000..5463bf983 --- /dev/null +++ b/pkg/recommendation/recommender/volumes/recommend.go @@ -0,0 +1,20 @@ +package volumes + +import ( + "github.com/gocrane/crane/pkg/recommendation/framework" +) + +func (s *VolumesRecommender) PreRecommend(ctx *framework.RecommendationContext) error { + return nil +} + +func (s *VolumesRecommender) Recommend(ctx *framework.RecommendationContext) error { + ctx.Recommendation.Status.Action = "Delete" + ctx.Recommendation.Status.Description = "It is an Orphan Volumes" + return nil +} + +// Policy add some logic for result of recommend phase. +func (s *VolumesRecommender) Policy(ctx *framework.RecommendationContext) error { + return nil +} diff --git a/pkg/recommendation/recommender/volumes/registry.go b/pkg/recommendation/recommender/volumes/registry.go new file mode 100644 index 000000000..be242a8d6 --- /dev/null +++ b/pkg/recommendation/recommender/volumes/registry.go @@ -0,0 +1,31 @@ +package volumes + +import ( + analysisv1alph1 "github.com/gocrane/api/analysis/v1alpha1" + "github.com/gocrane/crane/pkg/recommendation/config" + "github.com/gocrane/crane/pkg/recommendation/recommender" + "github.com/gocrane/crane/pkg/recommendation/recommender/apis" + "github.com/gocrane/crane/pkg/recommendation/recommender/base" +) + +var _ recommender.Recommender = &VolumesRecommender{} + +type VolumesRecommender struct { + base.BaseRecommender +} + +func init() { + recommender.RegisterRecommenderProvider(recommender.VolumesRecommender, NewVolumesRecommender) +} + +func (s *VolumesRecommender) Name() string { + return recommender.VolumesRecommender +} + +// NewVolumesRecommender create a new Volumes recommender. +func NewVolumesRecommender(recommender apis.Recommender, recommendationRule analysisv1alph1.RecommendationRule) (recommender.Recommender, error) { + recommender = config.MergeRecommenderConfigFromRule(recommender, recommendationRule) + return &VolumesRecommender{ + *base.NewBaseRecommender(recommender), + }, nil +} diff --git a/pkg/utils/pod.go b/pkg/utils/pod.go index 22d40547e..505ec3c9d 100644 --- a/pkg/utils/pod.go +++ b/pkg/utils/pod.go @@ -312,6 +312,43 @@ func GetNodePods(kubeClient client.Client, nodeName string) ([]corev1.Pod, error return podList.Items, nil } +// GetOrphanVolumes returns Orphan Volumes +func GetOrphanVolumes(kubeClient client.Client) ([]string, error) { + // Get a list of all volumes + volumes := &corev1.PersistentVolumeList{} + if err := kubeClient.List(context.Background(), volumes); err != nil { + return nil, err + } + + // Get a list of all pods + pods := &corev1.PodList{} + if err := kubeClient.List(context.Background(), pods); err != nil { + return nil, err + } + + // Check if each volume is being used by any pods + orphanVolumesName := []string{} + for _, volume := range volumes.Items { + if isOrphanVolume(&volume, pods) { + orphanVolumesName = append(orphanVolumesName, volume.Spec.ClaimRef.Name) + } + } + + return orphanVolumesName, nil +} + +// volume is not being used by any pod +func isOrphanVolume(volume *corev1.PersistentVolume, pods *corev1.PodList) bool { + for _, pod := range pods.Items { + for _, volumeClaim := range pod.Spec.Volumes { + if volumeClaim.PersistentVolumeClaim != nil && volumeClaim.PersistentVolumeClaim.ClaimName == volume.Spec.ClaimRef.Name { + return false + } + } + } + return true +} + func IsPodTerminated(pod *corev1.Pod) bool { return pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed }