From 68f5b7b9f0076d12d9a8ce0f027bb82daaabbd4d Mon Sep 17 00:00:00 2001 From: Jeff McCormick Date: Wed, 27 Oct 2021 21:06:15 -0500 Subject: [PATCH] add a prune package to handle cleanup of pods and job resources (#75) * add initial prune logic * add unit test, fix lint errors * update test * add prune job test * cleanup prune test * cleanup a few things * address review comments Co-authored-by: churromechanic --- prune/maxage.go | 43 +++++ prune/maxcount.go | 46 ++++++ prune/prune.go | 176 ++++++++++++++++++++ prune/prune_suite_test.go | 27 +++ prune/remove.go | 53 ++++++ prune/resource_test.go | 335 ++++++++++++++++++++++++++++++++++++++ prune/resources.go | 106 ++++++++++++ 7 files changed, 786 insertions(+) create mode 100644 prune/maxage.go create mode 100644 prune/maxcount.go create mode 100644 prune/prune.go create mode 100644 prune/prune_suite_test.go create mode 100644 prune/remove.go create mode 100644 prune/resource_test.go create mode 100644 prune/resources.go diff --git a/prune/maxage.go b/prune/maxage.go new file mode 100644 index 0000000..ad646cb --- /dev/null +++ b/prune/maxage.go @@ -0,0 +1,43 @@ +// Copyright 2021 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prune + +import ( + "context" + "time" +) + +// maxAge looks for and prunes resources, currently jobs and pods, +// that exceed a user specified age (e.g. 3d) +func pruneByMaxAge(ctx context.Context, config Config, resources []ResourceInfo) (err error) { + config.log.V(1).Info("maxAge running", "setting", config.Strategy.MaxAgeSetting) + + maxAgeDuration, _ := time.ParseDuration(config.Strategy.MaxAgeSetting) + maxAgeTime := time.Now().Add(-maxAgeDuration) + + for i := 0; i < len(resources); i++ { + config.log.V(1).Info("age of pod ", "age", time.Since(resources[i].StartTime), "maxage", maxAgeTime) + if resources[i].StartTime.Before(maxAgeTime) { + config.log.V(1).Info("pruning ", "kind", resources[i].GVK, "name", resources[i].Name) + + err := config.removeResource(ctx, resources[i]) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/prune/maxcount.go b/prune/maxcount.go new file mode 100644 index 0000000..2f909a5 --- /dev/null +++ b/prune/maxcount.go @@ -0,0 +1,46 @@ +// Copyright 2021 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prune + +import ( + "context" + "time" +) + +// pruneByMaxCount looks for and prunes resources, currently jobs and pods, +// that exceed a user specified count (e.g. 3), the oldest resources +// are pruned +func pruneByMaxCount(ctx context.Context, config Config, resources []ResourceInfo) (err error) { + config.log.V(1).Info("pruneByMaxCount running ", "max count", config.Strategy.MaxCountSetting, "resource count", len(resources)) + + if len(resources) > config.Strategy.MaxCountSetting { + removeCount := len(resources) - config.Strategy.MaxCountSetting + for i := len(resources) - 1; i >= 0; i-- { + config.log.V(1).Info("pruning pod ", "pod name", resources[i].Name, "age", time.Since(resources[i].StartTime)) + + err := config.removeResource(ctx, resources[i]) + if err != nil { + return err + } + + removeCount-- + if removeCount == 0 { + break + } + } + } + + return nil +} diff --git a/prune/prune.go b/prune/prune.go new file mode 100644 index 0000000..3b66b2d --- /dev/null +++ b/prune/prune.go @@ -0,0 +1,176 @@ +// Copyright 2021 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prune + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" +) + +// ResourceStatus describes the Kubernetes resource status we are evaluating +type ResourceStatus string + +// Strategy describes the pruning strategy we want to employ +type Strategy string + +const ( + // CustomStrategy maximum age of a resource that is desired, Duration + CustomStrategy Strategy = "Custom" + // MaxAgeStrategy maximum age of a resource that is desired, Duration + MaxAgeStrategy Strategy = "MaxAge" + // MaxCountStrategy maximum number of a resource that is desired, int + MaxCountStrategy Strategy = "MaxCount" + // JobKind equates to a Kube Job resource kind + JobKind string = "Job" + // PodKind equates to a Kube Pod resource kind + PodKind string = "Pod" +) + +// StrategyConfig holds settings unique to each pruning mode +type StrategyConfig struct { + Mode Strategy + MaxAgeSetting string + MaxCountSetting int + CustomSettings map[string]interface{} +} + +// StrategyFunc function allows a means to specify +// custom prune strategies +type StrategyFunc func(cfg Config, resources []ResourceInfo) error + +// PreDelete function is called before a resource is pruned +type PreDelete func(cfg Config, something ResourceInfo) error + +// Config defines a pruning configuration and ultimately +// determines what will get pruned +type Config struct { + Clientset kubernetes.Interface // kube client used by pruning + LabelSelector string //selector resources to prune + DryRun bool //true only performs a check, not removals + Resources []schema.GroupVersionKind //pods, jobs are supported + Namespaces []string //empty means all namespaces + Strategy StrategyConfig //strategy for pruning, either age or max + CustomStrategy StrategyFunc //custom strategy + PreDeleteHook PreDelete //called before resource is deleteds + log logr.Logger +} + +// Execute causes the pruning work to be executed based on its configuration +func (config Config) Execute(ctx context.Context) error { + + config.log.V(1).Info("Execute Prune") + + err := config.validate() + if err != nil { + return err + } + + for i := 0; i < len(config.Resources); i++ { + var resourceList []ResourceInfo + var err error + + if config.Resources[i].Kind == PodKind { + resourceList, err = config.getSucceededPods(ctx) + if err != nil { + return err + } + config.log.V(1).Info("pods ", "count", len(resourceList)) + } else if config.Resources[i].Kind == JobKind { + resourceList, err = config.getCompletedJobs(ctx) + if err != nil { + return err + } + config.log.V(1).Info("jobs ", "count", len(resourceList)) + } + + switch config.Strategy.Mode { + case MaxAgeStrategy: + err = pruneByMaxAge(ctx, config, resourceList) + case MaxCountStrategy: + err = pruneByMaxCount(ctx, config, resourceList) + case CustomStrategy: + err = config.CustomStrategy(config, resourceList) + default: + return fmt.Errorf("unknown strategy") + } + if err != nil { + return err + } + } + + config.log.V(1).Info("Prune completed") + + return nil +} + +// containsString checks if a string is present in a slice +func containsString(s []string, str string) bool { + for _, v := range s { + if v == str { + return true + } + } + + return false +} + +// containsName checks if a string is present in a ResourceInfo slice +func containsName(s []ResourceInfo, str string) bool { + for _, v := range s { + if v.Name == str { + return true + } + } + + return false +} +func (config Config) validate() (err error) { + + if config.CustomStrategy == nil && config.Strategy.Mode == CustomStrategy { + return fmt.Errorf("custom strategies require a strategy function to be specified") + } + + if len(config.Namespaces) == 0 { + return fmt.Errorf("namespaces are required") + } + + if containsString(config.Namespaces, "") { + return fmt.Errorf("empty namespace value not supported") + } + + _, err = labels.Parse(config.LabelSelector) + if err != nil { + return err + } + + if config.Strategy.Mode == MaxAgeStrategy { + _, err = time.ParseDuration(config.Strategy.MaxAgeSetting) + if err != nil { + return err + } + } + if config.Strategy.Mode == MaxCountStrategy { + if config.Strategy.MaxCountSetting < 0 { + return fmt.Errorf("max count is required to be greater than or equal to 0") + } + } + return nil +} diff --git a/prune/prune_suite_test.go b/prune/prune_suite_test.go new file mode 100644 index 0000000..6cfdd6b --- /dev/null +++ b/prune/prune_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2021 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prune + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestPrune(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Prune Suite") +} diff --git a/prune/remove.go b/prune/remove.go new file mode 100644 index 0000000..ee753dc --- /dev/null +++ b/prune/remove.go @@ -0,0 +1,53 @@ +// Copyright 2021 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prune + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (config Config) removeResource(ctx context.Context, resource ResourceInfo) (err error) { + + if config.DryRun { + return nil + } + + if config.PreDeleteHook != nil { + err = config.PreDeleteHook(config, resource) + if err != nil { + return err + } + } + + switch resource.GVK.Kind { + case PodKind: + err := config.Clientset.CoreV1().Pods(resource.Namespace).Delete(ctx, resource.Name, metav1.DeleteOptions{}) + if err != nil { + return err + } + case JobKind: + err := config.Clientset.BatchV1().Jobs(resource.Namespace).Delete(ctx, resource.Name, metav1.DeleteOptions{}) + if err != nil { + return err + } + default: + return fmt.Errorf("unsupported resource kind") + } + + return nil +} diff --git a/prune/resource_test.go b/prune/resource_test.go new file mode 100644 index 0000000..2639cdf --- /dev/null +++ b/prune/resource_test.go @@ -0,0 +1,335 @@ +// Copyright 2021 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prune + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" + testclient "k8s.io/client-go/kubernetes/fake" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +var _ = Describe("Prune", func() { + Describe("test pods", func() { + var ( + client kubernetes.Interface + cfg Config + ctx context.Context + ) + BeforeEach(func() { + client = testclient.NewSimpleClientset() + ctx = context.Background() + cfg = Config{ + log: logf.Log.WithName("prune"), + DryRun: false, + Clientset: client, + LabelSelector: "app=churro", + Resources: []schema.GroupVersionKind{ + {Group: "", Version: "", Kind: PodKind}, + }, + Namespaces: []string{"default"}, + Strategy: StrategyConfig{ + Mode: MaxCountStrategy, + MaxCountSetting: 1, + }, + PreDeleteHook: myhook, + } + + _ = createTestPods(client) + }) + It("test pod maxCount strategy", func() { + err := cfg.Execute(ctx) + Expect(err).Should(BeNil()) + var pods []ResourceInfo + pods, err = cfg.getSucceededPods(ctx) + Expect(err).Should(BeNil()) + Expect(len(pods)).To(Equal(1)) + Expect(containsName(pods, "churro1")).To(Equal(true)) + }) + It("test pod maxAge strategy", func() { + cfg.Strategy.Mode = MaxAgeStrategy + cfg.Strategy.MaxAgeSetting = "3h" + err := cfg.Execute(ctx) + Expect(err).Should(BeNil()) + var pods []ResourceInfo + pods, err = cfg.getSucceededPods(ctx) + Expect(err).Should(BeNil()) + Expect(containsName(pods, "churro1")).To(Equal(true)) + Expect(containsName(pods, "churro2")).To(Equal(true)) + }) + It("test pod custom strategy", func() { + cfg.Strategy.Mode = CustomStrategy + cfg.Strategy.CustomSettings = make(map[string]interface{}) + cfg.CustomStrategy = myStrategy + err := cfg.Execute(ctx) + Expect(err).Should(BeNil()) + var pods []ResourceInfo + pods, err = cfg.getSucceededPods(ctx) + Expect(err).Should(BeNil()) + Expect(len(pods)).To(Equal(3)) + }) + }) + + Describe("config validation", func() { + var ( + ctx context.Context + cfg Config + ) + BeforeEach(func() { + cfg = Config{} + cfg.log = logf.Log.WithName("prune") + ctx = context.Background() + }) + It("should return an error when LabelSelector is not set", func() { + err := cfg.Execute(ctx) + Expect(err).ShouldNot(BeNil()) + }) + It("should return an error is Namespaces is empty", func() { + cfg.LabelSelector = "app=churro" + err := cfg.Execute(ctx) + Expect(err).ShouldNot(BeNil()) + }) + It("should return an error when labels dont parse", func() { + cfg.Namespaces = []string{"one"} + cfg.LabelSelector = "-" + err := cfg.Execute(ctx) + Expect(err).ShouldNot(BeNil()) + }) + }) + + Describe("test jobs", func() { + var ( + jobclient kubernetes.Interface + jobcfg Config + ctx context.Context + ) + BeforeEach(func() { + jobclient = testclient.NewSimpleClientset() + + ctx = context.Background() + jobcfg = Config{ + DryRun: false, + log: logf.Log.WithName("prune"), + Clientset: jobclient, + LabelSelector: "app=churro", + Resources: []schema.GroupVersionKind{ + {Group: "", Version: "", Kind: JobKind}, + }, + Namespaces: []string{"default"}, + Strategy: StrategyConfig{ + Mode: MaxCountStrategy, + MaxCountSetting: 1, + }, + PreDeleteHook: myhook, + } + + _ = createTestJobs(jobclient) + }) + It("test job maxAge strategy", func() { + jobcfg.Strategy.Mode = MaxAgeStrategy + jobcfg.Strategy.MaxAgeSetting = "3h" + err := jobcfg.Execute(ctx) + Expect(err).Should(BeNil()) + var jobs []ResourceInfo + jobs, err = jobcfg.getCompletedJobs(ctx) + Expect(err).Should(BeNil()) + Expect(containsName(jobs, "churro1")).To(Equal(true)) + Expect(containsName(jobs, "churro2")).To(Equal(true)) + }) + It("test job maxCount strategy", func() { + err := jobcfg.Execute(ctx) + Expect(err).Should(BeNil()) + var jobs []ResourceInfo + jobs, err = jobcfg.getCompletedJobs(ctx) + Expect(err).Should(BeNil()) + Expect(len(jobs)).To(Equal(1)) + Expect(containsName(jobs, "churro1")).To(Equal(true)) + }) + It("test job custom strategy", func() { + jobcfg.Strategy.Mode = CustomStrategy + jobcfg.Strategy.CustomSettings = make(map[string]interface{}) + jobcfg.CustomStrategy = myStrategy + err := jobcfg.Execute(ctx) + Expect(err).Should(BeNil()) + var jobs []ResourceInfo + jobs, err = jobcfg.getCompletedJobs(ctx) + Expect(err).Should(BeNil()) + Expect(len(jobs)).To(Equal(3)) + }) + }) +}) + +// create 3 jobs with different start times (now, 2 days old, 4 days old) +func createTestJobs(client kubernetes.Interface) (err error) { + // some defaults + ns := "default" + labels := make(map[string]string) + labels["app"] = "churro" + + // delete any existing jobs + _ = client.BatchV1().Jobs(ns).Delete(context.TODO(), "churro1", metav1.DeleteOptions{}) + _ = client.BatchV1().Jobs(ns).Delete(context.TODO(), "churro2", metav1.DeleteOptions{}) + _ = client.BatchV1().Jobs(ns).Delete(context.TODO(), "churro3", metav1.DeleteOptions{}) + + // create 3 jobs with different CompletionTime + now := time.Now() //initial start time + startTime := metav1.NewTime(now) + j1 := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "churro1", + Namespace: ns, + Labels: labels, + }, + Status: batchv1.JobStatus{ + CompletionTime: &startTime, + }, + } + _, err = client.BatchV1().Jobs(ns).Create(context.TODO(), j1, metav1.CreateOptions{}) + if err != nil { + return err + } + + twoHoursPriorToNow := now.Add(time.Hour * time.Duration(-2)) + // create start time 2 hours before now + startTime = metav1.NewTime(twoHoursPriorToNow) + j2 := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "churro2", + Namespace: ns, + Labels: labels, + }, + Status: batchv1.JobStatus{ + CompletionTime: &startTime, + }, + } + _, err = client.BatchV1().Jobs(ns).Create(context.TODO(), j2, metav1.CreateOptions{}) + if err != nil { + return err + } + // create start time 4 hours before now + fourHoursPriorToNow := now.Add(time.Hour * time.Duration(-4)) + startTime = metav1.NewTime(fourHoursPriorToNow) + j3 := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "churro3", + Namespace: ns, + Labels: labels, + }, + Status: batchv1.JobStatus{ + CompletionTime: &startTime, + }, + } + _, err = client.BatchV1().Jobs(ns).Create(context.TODO(), j3, metav1.CreateOptions{}) + if err != nil { + return err + } + return nil +} + +// create 3 pods and 3 jobs with different start times (now, 2 days old, 4 days old) +func createTestPods(client kubernetes.Interface) (err error) { + // some defaults + ns := "default" + labels := make(map[string]string) + labels["app"] = "churro" + + // delete any existing pods + _ = client.CoreV1().Pods(ns).Delete(context.TODO(), "churro1", metav1.DeleteOptions{}) + _ = client.CoreV1().Pods(ns).Delete(context.TODO(), "churro2", metav1.DeleteOptions{}) + _ = client.CoreV1().Pods(ns).Delete(context.TODO(), "churro3", metav1.DeleteOptions{}) + + // create 3 pods with different StartTimes + now := time.Now() //initial start time + startTime := metav1.NewTime(now) + p1 := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "churro1", + Namespace: ns, + Labels: labels, + }, + Status: v1.PodStatus{ + Phase: v1.PodSucceeded, + StartTime: &startTime, + }, + } + _, err = client.CoreV1().Pods(ns).Create(context.TODO(), p1, metav1.CreateOptions{}) + if err != nil { + return err + } + + twoHoursPriorToNow := now.Add(time.Hour * time.Duration(-2)) + // create start time 2 hours before now + startTime = metav1.NewTime(twoHoursPriorToNow) + p2 := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "churro2", + Namespace: ns, + Labels: labels, + }, + Status: v1.PodStatus{ + Phase: v1.PodSucceeded, + StartTime: &startTime, + }, + } + _, err = client.CoreV1().Pods(ns).Create(context.TODO(), p2, metav1.CreateOptions{}) + if err != nil { + return err + } + // create start time 4 hours before now + fourHoursPriorToNow := now.Add(time.Hour * time.Duration(-4)) + startTime = metav1.NewTime(fourHoursPriorToNow) + p3 := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "churro3", + Namespace: ns, + Labels: labels, + }, + Status: v1.PodStatus{ + Phase: v1.PodSucceeded, + StartTime: &startTime, + }, + } + _, err = client.CoreV1().Pods(ns).Create(context.TODO(), p3, metav1.CreateOptions{}) + if err != nil { + return err + } + + return nil +} + +func myhook(cfg Config, x ResourceInfo) error { + fmt.Println("myhook is called ") + return nil +} + +// myStrategy shows how you can write your own strategy, in this +// example, the strategy doesn't really do another other than count +// the number of resources +func myStrategy(cfg Config, resources []ResourceInfo) error { + fmt.Printf("myStrategy is called with resources %v config %v\n", resources, cfg) + if len(resources) != 3 { + return fmt.Errorf("count of resources did not equal our expectation") + } + return nil +} diff --git a/prune/resources.go b/prune/resources.go new file mode 100644 index 0000000..f3e7cbf --- /dev/null +++ b/prune/resources.go @@ -0,0 +1,106 @@ +// Copyright 2021 The Operator-SDK Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prune + +import ( + "context" + "sort" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// ResourceInfo describes the Kube resources that we are about to consider +// when pruning resources +type ResourceInfo struct { + Name string + GVK schema.GroupVersionKind + Namespace string + StartTime time.Time +} + +func (config Config) getSucceededPods(ctx context.Context) (resources []ResourceInfo, err error) { + + listOptions := metav1.ListOptions{LabelSelector: config.LabelSelector} + for n := 0; n < len(config.Namespaces); n++ { + pods, err := config.Clientset.CoreV1().Pods(config.Namespaces[n]).List(ctx, listOptions) + if err != nil { + return resources, err + } + + for i := 0; i < len(pods.Items); i++ { + p := pods.Items[i] + switch p.Status.Phase { + case v1.PodRunning: + case v1.PodPending: + case v1.PodFailed: + case v1.PodUnknown: + case v1.PodSucceeded: + // currently we only care to prune succeeded pods + resources = append(resources, ResourceInfo{ + Name: p.Name, + GVK: schema.GroupVersionKind{ + Kind: PodKind, + }, + Namespace: config.Namespaces[n], + StartTime: p.Status.StartTime.Time, + }) + default: + } + } + } + + // sort by StartTime, earliest first order + sort.Slice(resources, func(i, j int) bool { + return resources[i].StartTime.After(resources[j].StartTime) + }) + + return resources, nil +} + +func (config Config) getCompletedJobs(ctx context.Context) (resources []ResourceInfo, err error) { + + listOptions := metav1.ListOptions{LabelSelector: config.LabelSelector} + + for n := 0; n < len(config.Namespaces); n++ { + jobs, err := config.Clientset.BatchV1().Jobs(config.Namespaces[n]).List(ctx, listOptions) + if err != nil { + return resources, err + } + for i := 0; i < len(jobs.Items); i++ { + j := jobs.Items[i] + if j.Status.CompletionTime != nil { + // currently we only care to prune succeeded pods + resources = append(resources, ResourceInfo{ + Name: j.Name, + GVK: schema.GroupVersionKind{ + Kind: JobKind, + }, + Namespace: config.Namespaces[n], + StartTime: j.Status.CompletionTime.Time, + }) + } + } + } + + // sort by StartTime, earliest first order + sort.Slice(resources, func(i, j int) bool { + return resources[i].StartTime.After(resources[j].StartTime) + }) + + return resources, nil +}