Skip to content

Commit 68f5b7b

Browse files
Jeff McCormickchurrodata
andauthored
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 <[email protected]>
1 parent d4e70d9 commit 68f5b7b

File tree

7 files changed

+786
-0
lines changed

7 files changed

+786
-0
lines changed

prune/maxage.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2021 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package prune
16+
17+
import (
18+
"context"
19+
"time"
20+
)
21+
22+
// maxAge looks for and prunes resources, currently jobs and pods,
23+
// that exceed a user specified age (e.g. 3d)
24+
func pruneByMaxAge(ctx context.Context, config Config, resources []ResourceInfo) (err error) {
25+
config.log.V(1).Info("maxAge running", "setting", config.Strategy.MaxAgeSetting)
26+
27+
maxAgeDuration, _ := time.ParseDuration(config.Strategy.MaxAgeSetting)
28+
maxAgeTime := time.Now().Add(-maxAgeDuration)
29+
30+
for i := 0; i < len(resources); i++ {
31+
config.log.V(1).Info("age of pod ", "age", time.Since(resources[i].StartTime), "maxage", maxAgeTime)
32+
if resources[i].StartTime.Before(maxAgeTime) {
33+
config.log.V(1).Info("pruning ", "kind", resources[i].GVK, "name", resources[i].Name)
34+
35+
err := config.removeResource(ctx, resources[i])
36+
if err != nil {
37+
return err
38+
}
39+
}
40+
}
41+
42+
return nil
43+
}

prune/maxcount.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2021 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package prune
16+
17+
import (
18+
"context"
19+
"time"
20+
)
21+
22+
// pruneByMaxCount looks for and prunes resources, currently jobs and pods,
23+
// that exceed a user specified count (e.g. 3), the oldest resources
24+
// are pruned
25+
func pruneByMaxCount(ctx context.Context, config Config, resources []ResourceInfo) (err error) {
26+
config.log.V(1).Info("pruneByMaxCount running ", "max count", config.Strategy.MaxCountSetting, "resource count", len(resources))
27+
28+
if len(resources) > config.Strategy.MaxCountSetting {
29+
removeCount := len(resources) - config.Strategy.MaxCountSetting
30+
for i := len(resources) - 1; i >= 0; i-- {
31+
config.log.V(1).Info("pruning pod ", "pod name", resources[i].Name, "age", time.Since(resources[i].StartTime))
32+
33+
err := config.removeResource(ctx, resources[i])
34+
if err != nil {
35+
return err
36+
}
37+
38+
removeCount--
39+
if removeCount == 0 {
40+
break
41+
}
42+
}
43+
}
44+
45+
return nil
46+
}

prune/prune.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2021 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package prune
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"time"
21+
22+
"github.com/go-logr/logr"
23+
"k8s.io/apimachinery/pkg/labels"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
25+
"k8s.io/client-go/kubernetes"
26+
)
27+
28+
// ResourceStatus describes the Kubernetes resource status we are evaluating
29+
type ResourceStatus string
30+
31+
// Strategy describes the pruning strategy we want to employ
32+
type Strategy string
33+
34+
const (
35+
// CustomStrategy maximum age of a resource that is desired, Duration
36+
CustomStrategy Strategy = "Custom"
37+
// MaxAgeStrategy maximum age of a resource that is desired, Duration
38+
MaxAgeStrategy Strategy = "MaxAge"
39+
// MaxCountStrategy maximum number of a resource that is desired, int
40+
MaxCountStrategy Strategy = "MaxCount"
41+
// JobKind equates to a Kube Job resource kind
42+
JobKind string = "Job"
43+
// PodKind equates to a Kube Pod resource kind
44+
PodKind string = "Pod"
45+
)
46+
47+
// StrategyConfig holds settings unique to each pruning mode
48+
type StrategyConfig struct {
49+
Mode Strategy
50+
MaxAgeSetting string
51+
MaxCountSetting int
52+
CustomSettings map[string]interface{}
53+
}
54+
55+
// StrategyFunc function allows a means to specify
56+
// custom prune strategies
57+
type StrategyFunc func(cfg Config, resources []ResourceInfo) error
58+
59+
// PreDelete function is called before a resource is pruned
60+
type PreDelete func(cfg Config, something ResourceInfo) error
61+
62+
// Config defines a pruning configuration and ultimately
63+
// determines what will get pruned
64+
type Config struct {
65+
Clientset kubernetes.Interface // kube client used by pruning
66+
LabelSelector string //selector resources to prune
67+
DryRun bool //true only performs a check, not removals
68+
Resources []schema.GroupVersionKind //pods, jobs are supported
69+
Namespaces []string //empty means all namespaces
70+
Strategy StrategyConfig //strategy for pruning, either age or max
71+
CustomStrategy StrategyFunc //custom strategy
72+
PreDeleteHook PreDelete //called before resource is deleteds
73+
log logr.Logger
74+
}
75+
76+
// Execute causes the pruning work to be executed based on its configuration
77+
func (config Config) Execute(ctx context.Context) error {
78+
79+
config.log.V(1).Info("Execute Prune")
80+
81+
err := config.validate()
82+
if err != nil {
83+
return err
84+
}
85+
86+
for i := 0; i < len(config.Resources); i++ {
87+
var resourceList []ResourceInfo
88+
var err error
89+
90+
if config.Resources[i].Kind == PodKind {
91+
resourceList, err = config.getSucceededPods(ctx)
92+
if err != nil {
93+
return err
94+
}
95+
config.log.V(1).Info("pods ", "count", len(resourceList))
96+
} else if config.Resources[i].Kind == JobKind {
97+
resourceList, err = config.getCompletedJobs(ctx)
98+
if err != nil {
99+
return err
100+
}
101+
config.log.V(1).Info("jobs ", "count", len(resourceList))
102+
}
103+
104+
switch config.Strategy.Mode {
105+
case MaxAgeStrategy:
106+
err = pruneByMaxAge(ctx, config, resourceList)
107+
case MaxCountStrategy:
108+
err = pruneByMaxCount(ctx, config, resourceList)
109+
case CustomStrategy:
110+
err = config.CustomStrategy(config, resourceList)
111+
default:
112+
return fmt.Errorf("unknown strategy")
113+
}
114+
if err != nil {
115+
return err
116+
}
117+
}
118+
119+
config.log.V(1).Info("Prune completed")
120+
121+
return nil
122+
}
123+
124+
// containsString checks if a string is present in a slice
125+
func containsString(s []string, str string) bool {
126+
for _, v := range s {
127+
if v == str {
128+
return true
129+
}
130+
}
131+
132+
return false
133+
}
134+
135+
// containsName checks if a string is present in a ResourceInfo slice
136+
func containsName(s []ResourceInfo, str string) bool {
137+
for _, v := range s {
138+
if v.Name == str {
139+
return true
140+
}
141+
}
142+
143+
return false
144+
}
145+
func (config Config) validate() (err error) {
146+
147+
if config.CustomStrategy == nil && config.Strategy.Mode == CustomStrategy {
148+
return fmt.Errorf("custom strategies require a strategy function to be specified")
149+
}
150+
151+
if len(config.Namespaces) == 0 {
152+
return fmt.Errorf("namespaces are required")
153+
}
154+
155+
if containsString(config.Namespaces, "") {
156+
return fmt.Errorf("empty namespace value not supported")
157+
}
158+
159+
_, err = labels.Parse(config.LabelSelector)
160+
if err != nil {
161+
return err
162+
}
163+
164+
if config.Strategy.Mode == MaxAgeStrategy {
165+
_, err = time.ParseDuration(config.Strategy.MaxAgeSetting)
166+
if err != nil {
167+
return err
168+
}
169+
}
170+
if config.Strategy.Mode == MaxCountStrategy {
171+
if config.Strategy.MaxCountSetting < 0 {
172+
return fmt.Errorf("max count is required to be greater than or equal to 0")
173+
}
174+
}
175+
return nil
176+
}

prune/prune_suite_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2021 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package prune
16+
17+
import (
18+
"testing"
19+
20+
. "github.com/onsi/ginkgo"
21+
. "github.com/onsi/gomega"
22+
)
23+
24+
func TestPrune(t *testing.T) {
25+
RegisterFailHandler(Fail)
26+
RunSpecs(t, "Prune Suite")
27+
}

prune/remove.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2021 The Operator-SDK Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package prune
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
)
23+
24+
func (config Config) removeResource(ctx context.Context, resource ResourceInfo) (err error) {
25+
26+
if config.DryRun {
27+
return nil
28+
}
29+
30+
if config.PreDeleteHook != nil {
31+
err = config.PreDeleteHook(config, resource)
32+
if err != nil {
33+
return err
34+
}
35+
}
36+
37+
switch resource.GVK.Kind {
38+
case PodKind:
39+
err := config.Clientset.CoreV1().Pods(resource.Namespace).Delete(ctx, resource.Name, metav1.DeleteOptions{})
40+
if err != nil {
41+
return err
42+
}
43+
case JobKind:
44+
err := config.Clientset.BatchV1().Jobs(resource.Namespace).Delete(ctx, resource.Name, metav1.DeleteOptions{})
45+
if err != nil {
46+
return err
47+
}
48+
default:
49+
return fmt.Errorf("unsupported resource kind")
50+
}
51+
52+
return nil
53+
}

0 commit comments

Comments
 (0)