/
cleaner.go
139 lines (121 loc) · 4.48 KB
/
cleaner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package envtestutil
import (
"context"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
. "github.com/onsi/gomega" //nolint:revive,golint,stylecheck // This is the standard for ginkgo and gomega.
)
// Cleaner is a struct to perform deletion of resources,
// enforcing removal of finalizers. Otherwise, deletion of namespaces wouldn't be possible.
// See: https://book.kubebuilder.io/reference/envtest.html#namespace-usage-limitation
// Based on https://github.com/kubernetes-sigs/controller-runtime/issues/880#issuecomment-749742403
type Cleaner struct {
clientset *kubernetes.Clientset
client client.Client
timeout, interval time.Duration
}
func CreateCleaner(c client.Client, config *rest.Config, timeout, interval time.Duration) *Cleaner {
k8sClient, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
}
return &Cleaner{
clientset: k8sClient,
client: c,
timeout: timeout,
interval: interval,
}
}
func (c *Cleaner) DeleteAll(objects ...client.Object) {
for _, obj := range objects {
obj := obj
Expect(client.IgnoreNotFound(c.client.Delete(context.Background(), obj))).Should(Succeed())
if ns, ok := obj.(*corev1.Namespace); ok {
// Normally the kube-controller-manager would handle finalization
// and garbage collection of namespaces, but with envtest, we aren't
// running a kube-controller-manager. Instead, we're going to approximate
// (poorly) the kube-controller-manager by explicitly deleting some
// resources within the namespace and then removing the `kubernetes`
// finalizer from the namespace resource, so it can finish deleting.
// Note that any resources within the namespace that we don't
// successfully delete could reappear if the namespace is ever
// recreated with the same name.
// Look up all namespaced resources under the discovery API
_, apiResources, err := c.clientset.DiscoveryClient.ServerGroupsAndResources()
Expect(err).ShouldNot(HaveOccurred())
namespacedGVKs := make(map[string]schema.GroupVersionKind)
for _, apiResourceList := range apiResources {
defaultGV, err := schema.ParseGroupVersion(apiResourceList.GroupVersion)
Expect(err).ShouldNot(HaveOccurred())
for _, r := range apiResourceList.APIResources {
if !r.Namespaced || strings.Contains(r.Name, "/") {
// skip non-namespaced and subresources
continue
}
gvk := schema.GroupVersionKind{
Group: defaultGV.Group,
Version: defaultGV.Version,
Kind: r.Kind,
}
if r.Group != "" {
gvk.Group = r.Group
}
if r.Version != "" {
gvk.Version = r.Version
}
namespacedGVKs[gvk.String()] = gvk
}
}
// Delete all namespaced resources in this namespace
for _, gvk := range namespacedGVKs {
var u unstructured.Unstructured
u.SetGroupVersionKind(gvk)
err := c.client.DeleteAllOf(context.Background(), &u, client.InNamespace(ns.Name))
Expect(client.IgnoreNotFound(ignoreMethodNotAllowed(err))).ShouldNot(HaveOccurred())
}
Eventually(func() error {
key := client.ObjectKeyFromObject(ns)
if err := c.client.Get(context.Background(), key, ns); err != nil {
return client.IgnoreNotFound(err)
}
// remove `kubernetes` finalizer
const k8s = "kubernetes"
finalizers := []corev1.FinalizerName{}
for _, f := range ns.Spec.Finalizers {
if f != k8s {
finalizers = append(finalizers, f)
}
}
ns.Spec.Finalizers = finalizers
// We have to use the k8s.io/client-go library here to expose
// ability to patch the /finalize subresource on the namespace
_, err = c.clientset.CoreV1().Namespaces().Finalize(context.Background(), ns, metav1.UpdateOptions{})
return err
}).
WithTimeout(c.timeout).
WithPolling(c.interval).
Should(Succeed())
}
Eventually(func() metav1.StatusReason {
key := client.ObjectKeyFromObject(obj)
if err := c.client.Get(context.Background(), key, obj); err != nil {
return apierrors.ReasonForError(err)
}
return ""
}, c.timeout, c.interval).Should(Equal(metav1.StatusReasonNotFound))
}
}
func ignoreMethodNotAllowed(err error) error {
if apierrors.ReasonForError(err) == metav1.StatusReasonMethodNotAllowed {
return nil
}
return err
}