Skip to content

Commit 7c32a02

Browse files
committed
feat(reset): add destroy volumes and destroy-load-balancers flag
Signed-off-by: rajaSahil <[email protected]>
1 parent 522bff6 commit 7c32a02

File tree

9 files changed

+517
-22
lines changed

9 files changed

+517
-22
lines changed

go.mod

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ require (
2626
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
2727
github.com/sirupsen/logrus v1.9.3
2828
github.com/spf13/cobra v1.8.1
29-
github.com/spf13/pflag v1.0.5
29+
github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace
3030
go.etcd.io/etcd/client/v3 v3.5.16
3131
golang.org/x/crypto v0.27.0
3232
golang.org/x/term v0.24.0
@@ -36,6 +36,7 @@ require (
3636
gopkg.in/yaml.v2 v2.4.0
3737
helm.sh/helm/v3 v3.16.1
3838
k8c.io/machine-controller v1.60.0
39+
k8c.io/reconciler v0.5.0
3940
k8s.io/api v0.31.1
4041
k8s.io/apiextensions-apiserver v0.31.1
4142
k8s.io/apimachinery v0.31.1
@@ -99,7 +100,7 @@ require (
99100
github.com/google/btree v1.0.1 // indirect
100101
github.com/google/gnostic-models v0.6.8 // indirect
101102
github.com/google/go-querystring v1.1.0 // indirect
102-
github.com/google/gofuzz v1.2.0 // indirect
103+
github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea // indirect
103104
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
104105
github.com/google/uuid v1.6.0 // indirect
105106
github.com/gorilla/mux v1.8.0 // indirect

go.sum

+6-3
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,8 @@ github.com/google/go-github/v65 v65.0.0/go.mod h1:DvrqWo5hvsdhJvHd4WyVF9ttANN3Bn
194194
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
195195
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
196196
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
197-
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
198-
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
197+
github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea h1:VcIYpAGBae3Z6BVncE0OnTE/ZjlDXqtYhOZky88neLM=
198+
github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
199199
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
200200
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
201201
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
@@ -376,8 +376,9 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
376376
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
377377
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
378378
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
379-
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
380379
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
380+
github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace h1:9PNP1jnUjRhfmGMlkXHjYPishpcw4jpSt/V/xYY3FMA=
381+
github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
381382
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
382383
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
383384
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
@@ -565,6 +566,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
565566
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
566567
k8c.io/machine-controller v1.60.0 h1:0ShjXyAnv0hpo59UsV9VFjEfgyG/2XrljBaEUV6JzwM=
567568
k8c.io/machine-controller v1.60.0/go.mod h1:j9SHRLpzFj5wOMlhdPJL+ub08P8rvVvQOFtg7JaLYb4=
569+
k8c.io/reconciler v0.5.0 h1:BHpelg1UfI/7oBFctqOq8sX6qzflXpl3SlvHe7e8wak=
570+
k8c.io/reconciler v0.5.0/go.mod h1:pT1+SVcVXJQeBJhpJBXQ5XW64QnKKeYTnVlQf0dGE0k=
568571
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
569572
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
570573
k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=

pkg/clientutil/service.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright 2020 The KubeOne Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package clientutil
18+
19+
import (
20+
"context"
21+
"time"
22+
23+
"k8c.io/kubeone/pkg/fail"
24+
25+
"github.com/sirupsen/logrus"
26+
corev1 "k8s.io/api/core/v1"
27+
"k8s.io/apimachinery/pkg/util/wait"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
)
30+
31+
func CleanupLBs(ctx context.Context, logger logrus.FieldLogger, c client.Client) error {
32+
serviceList := &corev1.ServiceList{}
33+
if err := c.List(ctx, serviceList); err != nil {
34+
return fail.KubeClient(err, "failed to list Service.")
35+
}
36+
37+
for _, service := range serviceList.Items {
38+
// This service is already in deletion, nothing further needs to happen.
39+
if service.DeletionTimestamp != nil {
40+
continue
41+
}
42+
// Only LoadBalancer services incur charges on cloud providers
43+
if service.Spec.Type == corev1.ServiceTypeLoadBalancer {
44+
logger.Infof("Deleting SVC : %s/%s\n", service.Namespace, service.Name)
45+
if err := DeleteIfExists(ctx, c, &service); err != nil {
46+
return err
47+
}
48+
}
49+
}
50+
51+
return nil
52+
}
53+
54+
func WaitCleanupLbs(ctx context.Context, logger logrus.FieldLogger, c client.Client) error {
55+
logger.Infoln("Waiting for all load balancer services to get deleted...")
56+
57+
return wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, false, func(ctx context.Context) (bool, error) {
58+
serviceList := &corev1.ServiceList{}
59+
if err := c.List(ctx, serviceList); err != nil {
60+
return false, nil
61+
}
62+
for _, service := range serviceList.Items {
63+
// Only LoadBalancer services incur charges on cloud providers
64+
if service.Spec.Type == corev1.ServiceTypeLoadBalancer {
65+
return false, nil
66+
}
67+
}
68+
69+
return true, nil
70+
})
71+
}

pkg/clientutil/volumes.go

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
Copyright 2020 The KubeOne Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package clientutil
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"time"
23+
24+
"k8c.io/kubeone/pkg/fail"
25+
"k8c.io/reconciler/pkg/reconciling"
26+
27+
"github.com/sirupsen/logrus"
28+
corev1 "k8s.io/api/core/v1"
29+
"k8s.io/apimachinery/pkg/util/wait"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
31+
)
32+
33+
const (
34+
annotationKeyDescription = "description"
35+
36+
// AnnDynamicallyProvisioned is added to a PV that is dynamically provisioned by kubernetes
37+
// Because the annotation is defined only at k8s.io/kubernetes, copying the content instead of vendoring
38+
// https://github.com/kubernetes/kubernetes/blob/v1.21.0/pkg/controller/volume/persistentvolume/util/util.go#L65
39+
AnnDynamicallyProvisioned = "pv.kubernetes.io/provisioned-by"
40+
)
41+
42+
var VolumeResources = []string{"persistentvolumes", "persistentvolumeclaims"}
43+
44+
func CleanupUnretainedVolumes(ctx context.Context, logger logrus.FieldLogger, c client.Client) error {
45+
// We disable the PV & PVC creation so nothing creates new PV's while we delete them
46+
logger.Infoln("Creating ValidatingWebhookConfiguration to disable future PV & PVC creation...")
47+
if err := disablePVCreation(ctx, c); err != nil {
48+
return fail.KubeClient(err, "failed to disable future PV & PVC creation.")
49+
}
50+
51+
pvcList, pvList, err := getDynamicallyProvisionedUnretainedPvs(ctx, c)
52+
if err != nil {
53+
return err
54+
}
55+
56+
// Do not attempt to delete any pods when there are no PVs and PVCs
57+
if (pvcList != nil && pvList != nil) && len(pvcList.Items) == 0 && len(pvList.Items) == 0 {
58+
return nil
59+
}
60+
61+
// Delete all Pods that use PVs. We must keep the remaining pods, otherwise
62+
// we end up in a deadlock when CSI is used
63+
if err := cleanupPVCUsingPods(ctx, c); err != nil {
64+
return fail.KubeClient(err, "failed to clean up PV using pod from user cluster.")
65+
}
66+
67+
// Delete PVC's
68+
logger.Infoln("Deleting persistent volume claims...")
69+
for _, pvc := range pvcList.Items {
70+
if pvc.DeletionTimestamp == nil {
71+
identifier := fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name)
72+
logger.Infoln("Deleting PVC...", identifier)
73+
74+
if err := DeleteIfExists(ctx, c, &pvc); err != nil {
75+
return fail.KubeClient(err, "failed to delete PVC from user cluster.")
76+
}
77+
}
78+
}
79+
80+
return nil
81+
}
82+
83+
func disablePVCreation(ctx context.Context, c client.Client) error {
84+
// Prevent re-creation of PVs and PVCs by using an intentionally defunct admissionWebhook
85+
creatorGetters := []reconciling.NamedValidatingWebhookConfigurationReconcilerFactory{
86+
creationPreventingWebhook("", VolumeResources),
87+
}
88+
if err := reconciling.ReconcileValidatingWebhookConfigurations(ctx, creatorGetters, "", c); err != nil {
89+
return fail.KubeClient(err, "failed to create ValidatingWebhookConfiguration to prevent creation of PVs/PVCs.")
90+
}
91+
92+
return nil
93+
}
94+
95+
func cleanupPVCUsingPods(ctx context.Context, c client.Client) error {
96+
podList := &corev1.PodList{}
97+
if err := c.List(ctx, podList); err != nil {
98+
return fail.KubeClient(err, "failed to list Pods from user cluster.")
99+
}
100+
101+
var pvUsingPods []*corev1.Pod
102+
for idx := range podList.Items {
103+
pod := &podList.Items[idx]
104+
if podUsesPV(pod) {
105+
pvUsingPods = append(pvUsingPods, pod)
106+
}
107+
}
108+
109+
for _, pod := range pvUsingPods {
110+
if pod.DeletionTimestamp == nil {
111+
if err := DeleteIfExists(ctx, c, pod); err != nil {
112+
return fail.KubeClient(err, "failed to delete Pod.")
113+
}
114+
}
115+
}
116+
117+
return nil
118+
}
119+
120+
func podUsesPV(p *corev1.Pod) bool {
121+
for _, volume := range p.Spec.Volumes {
122+
if volume.VolumeSource.PersistentVolumeClaim != nil {
123+
return true
124+
}
125+
}
126+
127+
return false
128+
}
129+
130+
func getDynamicallyProvisionedUnretainedPvs(ctx context.Context, c client.Client) (*corev1.PersistentVolumeClaimList, *corev1.PersistentVolumeList, error) {
131+
pvcList := &corev1.PersistentVolumeClaimList{}
132+
if err := c.List(ctx, pvcList); err != nil {
133+
return nil, nil, fail.KubeClient(err, "failed to list PVCs from user cluster.")
134+
}
135+
allPVList := &corev1.PersistentVolumeList{}
136+
if err := c.List(ctx, allPVList); err != nil {
137+
return nil, nil, fail.KubeClient(err, "failed to list PVs from user cluster.")
138+
}
139+
pvList := &corev1.PersistentVolumeList{}
140+
for _, pv := range allPVList.Items {
141+
// Check only dynamically provisioned PVs with delete reclaim policy to verify provisioner has done the cleanup
142+
// this filters out everything else because we leave those be
143+
if pv.Annotations[AnnDynamicallyProvisioned] != "" && pv.Spec.PersistentVolumeReclaimPolicy == corev1.PersistentVolumeReclaimDelete {
144+
pvList.Items = append(pvList.Items, pv)
145+
}
146+
}
147+
148+
return pvcList, pvList, nil
149+
}
150+
151+
func WaitCleanUpVolumes(ctx context.Context, logger logrus.FieldLogger, c client.Client) error {
152+
logger.Infoln("Waiting for all dynamically provisioned and unretained volumes to get deleted...")
153+
154+
return wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, false, func(ctx context.Context) (bool, error) {
155+
pvcList, pvList, err := getDynamicallyProvisionedUnretainedPvs(ctx, c)
156+
if err != nil {
157+
return false, nil
158+
}
159+
160+
if (pvcList != nil && pvList != nil) && len(pvcList.Items) == 0 && len(pvList.Items) == 0 {
161+
return true, nil
162+
}
163+
164+
return false, nil
165+
})
166+
}

pkg/clientutil/webhook.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
Copyright 2020 The KubeOne Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package clientutil
18+
19+
import (
20+
"context"
21+
"strings"
22+
23+
"k8c.io/kubeone/pkg/fail"
24+
"k8c.io/reconciler/pkg/reconciling"
25+
26+
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
27+
"k8s.io/apimachinery/pkg/types"
28+
"k8s.io/utils/ptr"
29+
"sigs.k8s.io/controller-runtime/pkg/client"
30+
)
31+
32+
// creationPreventingWebhook returns a ValidatingWebhookConfiguration that is intentionally defunct
33+
// and will prevent all creation requests from succeeding.
34+
func creationPreventingWebhook(apiGroup string, resources []string) reconciling.NamedValidatingWebhookConfigurationReconcilerFactory {
35+
failurePolicy := admissionregistrationv1.Fail
36+
sideEffects := admissionregistrationv1.SideEffectClassNone
37+
38+
return func() (string, reconciling.ValidatingWebhookConfigurationReconciler) {
39+
return "kubernetes-cluster-cleanup-" + strings.Join(resources, "-"),
40+
func(vwc *admissionregistrationv1.ValidatingWebhookConfiguration) (*admissionregistrationv1.ValidatingWebhookConfiguration, error) {
41+
if vwc.Annotations == nil {
42+
vwc.Annotations = map[string]string{}
43+
}
44+
vwc.Annotations[annotationKeyDescription] = "This webhook configuration exists to prevent creation of any new stateful resources in a cluster that is currently being terminated"
45+
46+
// This only gets set when the APIServer supports it, so carry it over
47+
var scope *admissionregistrationv1.ScopeType
48+
if len(vwc.Webhooks) != 1 {
49+
vwc.Webhooks = []admissionregistrationv1.ValidatingWebhook{{}}
50+
} else if len(vwc.Webhooks[0].Rules) > 0 {
51+
scope = vwc.Webhooks[0].Rules[0].Scope
52+
}
53+
// Must be a domain with at least three segments separated by dots
54+
vwc.Webhooks[0].Name = "kubernetes.cluster.cleanup"
55+
vwc.Webhooks[0].ClientConfig = admissionregistrationv1.WebhookClientConfig{
56+
URL: ptr.To("https://127.0.0.1:1"),
57+
}
58+
vwc.Webhooks[0].Rules = []admissionregistrationv1.RuleWithOperations{
59+
{
60+
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
61+
Rule: admissionregistrationv1.Rule{
62+
APIGroups: []string{apiGroup},
63+
APIVersions: []string{"*"},
64+
Resources: resources,
65+
Scope: scope,
66+
},
67+
},
68+
}
69+
vwc.Webhooks[0].FailurePolicy = &failurePolicy
70+
vwc.Webhooks[0].SideEffects = &sideEffects
71+
vwc.Webhooks[0].AdmissionReviewVersions = []string{"v1"}
72+
73+
return vwc, nil
74+
}
75+
}
76+
}
77+
78+
func DeletePreventingWebhook(ctx context.Context, c client.Client, resourceName string) error {
79+
vwc := admissionregistrationv1.ValidatingWebhookConfiguration{}
80+
if err := c.Get(ctx, types.NamespacedName{Name: resourceName}, &vwc); err != nil {
81+
return fail.KubeClient(err, "failed to get ValidatingWebhookConfiguration")
82+
}
83+
if err := DeleteIfExists(ctx, c, &vwc); err != nil {
84+
return err
85+
}
86+
87+
return nil
88+
}

0 commit comments

Comments
 (0)