Skip to content

Commit

Permalink
feat(reset): add destroy volumes and destroy-load-balancers flag
Browse files Browse the repository at this point in the history
Signed-off-by: rajaSahil <[email protected]>
  • Loading branch information
rajaSahil committed Dec 20, 2024
1 parent 522bff6 commit 7c32a02
Show file tree
Hide file tree
Showing 9 changed files with 517 additions and 22 deletions.
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace
go.etcd.io/etcd/client/v3 v3.5.16
golang.org/x/crypto v0.27.0
golang.org/x/term v0.24.0
Expand All @@ -36,6 +36,7 @@ require (
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.16.1
k8c.io/machine-controller v1.60.0
k8c.io/reconciler v0.5.0
k8s.io/api v0.31.1
k8s.io/apiextensions-apiserver v0.31.1
k8s.io/apimachinery v0.31.1
Expand Down Expand Up @@ -99,7 +100,7 @@ require (
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
Expand Down
9 changes: 6 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,8 @@ github.com/google/go-github/v65 v65.0.0/go.mod h1:DvrqWo5hvsdhJvHd4WyVF9ttANN3Bn
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea h1:VcIYpAGBae3Z6BVncE0OnTE/ZjlDXqtYhOZky88neLM=
github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
Expand Down Expand Up @@ -376,8 +376,9 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace h1:9PNP1jnUjRhfmGMlkXHjYPishpcw4jpSt/V/xYY3FMA=
github.com/spf13/pflag v1.0.6-0.20210604193023-d5e0c0615ace/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
Expand Down Expand Up @@ -565,6 +566,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8c.io/machine-controller v1.60.0 h1:0ShjXyAnv0hpo59UsV9VFjEfgyG/2XrljBaEUV6JzwM=
k8c.io/machine-controller v1.60.0/go.mod h1:j9SHRLpzFj5wOMlhdPJL+ub08P8rvVvQOFtg7JaLYb4=
k8c.io/reconciler v0.5.0 h1:BHpelg1UfI/7oBFctqOq8sX6qzflXpl3SlvHe7e8wak=
k8c.io/reconciler v0.5.0/go.mod h1:pT1+SVcVXJQeBJhpJBXQ5XW64QnKKeYTnVlQf0dGE0k=
k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
Expand Down
71 changes: 71 additions & 0 deletions pkg/clientutil/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
Copyright 2020 The KubeOne 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 clientutil

import (
"context"
"time"

"k8c.io/kubeone/pkg/fail"

"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func CleanupLBs(ctx context.Context, logger logrus.FieldLogger, c client.Client) error {
serviceList := &corev1.ServiceList{}
if err := c.List(ctx, serviceList); err != nil {
return fail.KubeClient(err, "failed to list Service.")
}

for _, service := range serviceList.Items {
// This service is already in deletion, nothing further needs to happen.
if service.DeletionTimestamp != nil {
continue
}
// Only LoadBalancer services incur charges on cloud providers
if service.Spec.Type == corev1.ServiceTypeLoadBalancer {
logger.Infof("Deleting SVC : %s/%s\n", service.Namespace, service.Name)
if err := DeleteIfExists(ctx, c, &service); err != nil {
return err
}
}
}

return nil
}

func WaitCleanupLbs(ctx context.Context, logger logrus.FieldLogger, c client.Client) error {
logger.Infoln("Waiting for all load balancer services to get deleted...")

return wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, false, func(ctx context.Context) (bool, error) {
serviceList := &corev1.ServiceList{}
if err := c.List(ctx, serviceList); err != nil {
return false, nil
}
for _, service := range serviceList.Items {
// Only LoadBalancer services incur charges on cloud providers
if service.Spec.Type == corev1.ServiceTypeLoadBalancer {
return false, nil
}
}

return true, nil
})
}
166 changes: 166 additions & 0 deletions pkg/clientutil/volumes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
Copyright 2020 The KubeOne 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 clientutil

import (
"context"
"fmt"
"time"

"k8c.io/kubeone/pkg/fail"
"k8c.io/reconciler/pkg/reconciling"

"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
annotationKeyDescription = "description"

// AnnDynamicallyProvisioned is added to a PV that is dynamically provisioned by kubernetes
// Because the annotation is defined only at k8s.io/kubernetes, copying the content instead of vendoring
// https://github.com/kubernetes/kubernetes/blob/v1.21.0/pkg/controller/volume/persistentvolume/util/util.go#L65
AnnDynamicallyProvisioned = "pv.kubernetes.io/provisioned-by"
)

var VolumeResources = []string{"persistentvolumes", "persistentvolumeclaims"}

func CleanupUnretainedVolumes(ctx context.Context, logger logrus.FieldLogger, c client.Client) error {
// We disable the PV & PVC creation so nothing creates new PV's while we delete them
logger.Infoln("Creating ValidatingWebhookConfiguration to disable future PV & PVC creation...")
if err := disablePVCreation(ctx, c); err != nil {
return fail.KubeClient(err, "failed to disable future PV & PVC creation.")
}

pvcList, pvList, err := getDynamicallyProvisionedUnretainedPvs(ctx, c)
if err != nil {
return err
}

// Do not attempt to delete any pods when there are no PVs and PVCs
if (pvcList != nil && pvList != nil) && len(pvcList.Items) == 0 && len(pvList.Items) == 0 {
return nil
}

// Delete all Pods that use PVs. We must keep the remaining pods, otherwise
// we end up in a deadlock when CSI is used
if err := cleanupPVCUsingPods(ctx, c); err != nil {
return fail.KubeClient(err, "failed to clean up PV using pod from user cluster.")
}

// Delete PVC's
logger.Infoln("Deleting persistent volume claims...")
for _, pvc := range pvcList.Items {
if pvc.DeletionTimestamp == nil {
identifier := fmt.Sprintf("%s/%s", pvc.Namespace, pvc.Name)
logger.Infoln("Deleting PVC...", identifier)

if err := DeleteIfExists(ctx, c, &pvc); err != nil {
return fail.KubeClient(err, "failed to delete PVC from user cluster.")
}
}
}

return nil
}

func disablePVCreation(ctx context.Context, c client.Client) error {
// Prevent re-creation of PVs and PVCs by using an intentionally defunct admissionWebhook
creatorGetters := []reconciling.NamedValidatingWebhookConfigurationReconcilerFactory{
creationPreventingWebhook("", VolumeResources),
}
if err := reconciling.ReconcileValidatingWebhookConfigurations(ctx, creatorGetters, "", c); err != nil {
return fail.KubeClient(err, "failed to create ValidatingWebhookConfiguration to prevent creation of PVs/PVCs.")
}

return nil
}

func cleanupPVCUsingPods(ctx context.Context, c client.Client) error {
podList := &corev1.PodList{}
if err := c.List(ctx, podList); err != nil {
return fail.KubeClient(err, "failed to list Pods from user cluster.")
}

var pvUsingPods []*corev1.Pod
for idx := range podList.Items {
pod := &podList.Items[idx]
if podUsesPV(pod) {
pvUsingPods = append(pvUsingPods, pod)
}
}

for _, pod := range pvUsingPods {
if pod.DeletionTimestamp == nil {
if err := DeleteIfExists(ctx, c, pod); err != nil {
return fail.KubeClient(err, "failed to delete Pod.")
}
}
}

return nil
}

func podUsesPV(p *corev1.Pod) bool {
for _, volume := range p.Spec.Volumes {
if volume.VolumeSource.PersistentVolumeClaim != nil {
return true
}
}

return false
}

func getDynamicallyProvisionedUnretainedPvs(ctx context.Context, c client.Client) (*corev1.PersistentVolumeClaimList, *corev1.PersistentVolumeList, error) {
pvcList := &corev1.PersistentVolumeClaimList{}
if err := c.List(ctx, pvcList); err != nil {
return nil, nil, fail.KubeClient(err, "failed to list PVCs from user cluster.")
}
allPVList := &corev1.PersistentVolumeList{}
if err := c.List(ctx, allPVList); err != nil {
return nil, nil, fail.KubeClient(err, "failed to list PVs from user cluster.")
}
pvList := &corev1.PersistentVolumeList{}
for _, pv := range allPVList.Items {
// Check only dynamically provisioned PVs with delete reclaim policy to verify provisioner has done the cleanup
// this filters out everything else because we leave those be
if pv.Annotations[AnnDynamicallyProvisioned] != "" && pv.Spec.PersistentVolumeReclaimPolicy == corev1.PersistentVolumeReclaimDelete {
pvList.Items = append(pvList.Items, pv)
}
}

return pvcList, pvList, nil
}

func WaitCleanUpVolumes(ctx context.Context, logger logrus.FieldLogger, c client.Client) error {
logger.Infoln("Waiting for all dynamically provisioned and unretained volumes to get deleted...")

return wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, false, func(ctx context.Context) (bool, error) {
pvcList, pvList, err := getDynamicallyProvisionedUnretainedPvs(ctx, c)
if err != nil {
return false, nil
}

if (pvcList != nil && pvList != nil) && len(pvcList.Items) == 0 && len(pvList.Items) == 0 {
return true, nil
}

return false, nil
})
}
88 changes: 88 additions & 0 deletions pkg/clientutil/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2020 The KubeOne 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 clientutil

import (
"context"
"strings"

"k8c.io/kubeone/pkg/fail"
"k8c.io/reconciler/pkg/reconciling"

admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// creationPreventingWebhook returns a ValidatingWebhookConfiguration that is intentionally defunct
// and will prevent all creation requests from succeeding.
func creationPreventingWebhook(apiGroup string, resources []string) reconciling.NamedValidatingWebhookConfigurationReconcilerFactory {
failurePolicy := admissionregistrationv1.Fail
sideEffects := admissionregistrationv1.SideEffectClassNone

return func() (string, reconciling.ValidatingWebhookConfigurationReconciler) {
return "kubernetes-cluster-cleanup-" + strings.Join(resources, "-"),
func(vwc *admissionregistrationv1.ValidatingWebhookConfiguration) (*admissionregistrationv1.ValidatingWebhookConfiguration, error) {
if vwc.Annotations == nil {
vwc.Annotations = map[string]string{}
}
vwc.Annotations[annotationKeyDescription] = "This webhook configuration exists to prevent creation of any new stateful resources in a cluster that is currently being terminated"

// This only gets set when the APIServer supports it, so carry it over
var scope *admissionregistrationv1.ScopeType
if len(vwc.Webhooks) != 1 {
vwc.Webhooks = []admissionregistrationv1.ValidatingWebhook{{}}
} else if len(vwc.Webhooks[0].Rules) > 0 {
scope = vwc.Webhooks[0].Rules[0].Scope
}
// Must be a domain with at least three segments separated by dots
vwc.Webhooks[0].Name = "kubernetes.cluster.cleanup"
vwc.Webhooks[0].ClientConfig = admissionregistrationv1.WebhookClientConfig{
URL: ptr.To("https://127.0.0.1:1"),
}
vwc.Webhooks[0].Rules = []admissionregistrationv1.RuleWithOperations{
{
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{apiGroup},
APIVersions: []string{"*"},
Resources: resources,
Scope: scope,
},
},
}
vwc.Webhooks[0].FailurePolicy = &failurePolicy
vwc.Webhooks[0].SideEffects = &sideEffects
vwc.Webhooks[0].AdmissionReviewVersions = []string{"v1"}

return vwc, nil
}
}
}

func DeletePreventingWebhook(ctx context.Context, c client.Client, resourceName string) error {
vwc := admissionregistrationv1.ValidatingWebhookConfiguration{}
if err := c.Get(ctx, types.NamespacedName{Name: resourceName}, &vwc); err != nil {
return fail.KubeClient(err, "failed to get ValidatingWebhookConfiguration")
}
if err := DeleteIfExists(ctx, c, &vwc); err != nil {
return err
}

return nil
}
Loading

0 comments on commit 7c32a02

Please sign in to comment.