From 748ec4e4131c56d11e441f5f1014dcab482c33e6 Mon Sep 17 00:00:00 2001
From: Jiaqi Luo <6218999+jiaqiluo@users.noreply.github.com>
Date: Mon, 15 Mar 2021 10:36:44 -0700
Subject: [PATCH] add a post-delete-hook for cleaning up the apps left after
 uninstalling Rancher

---
 chart/scripts/post-delete-hook.sh             | 85 +++++++++++++++++++
 ...post-delete-hook-cluster-role-binding.yaml | 19 +++++
 .../post-delete-hook-cluster-role.yaml        | 27 ++++++
 .../post-delete-hook-config-map.yaml          | 15 ++++
 chart/templates/post-delete-hook-job.yaml     | 44 ++++++++++
 .../post-delete-hook-service-account.yaml     | 12 +++
 chart/values.yaml                             | 15 ++++
 scripts/chart/build                           | 15 ++++
 8 files changed, 232 insertions(+)
 create mode 100755 chart/scripts/post-delete-hook.sh
 create mode 100644 chart/templates/post-delete-hook-cluster-role-binding.yaml
 create mode 100644 chart/templates/post-delete-hook-cluster-role.yaml
 create mode 100644 chart/templates/post-delete-hook-config-map.yaml
 create mode 100644 chart/templates/post-delete-hook-job.yaml
 create mode 100644 chart/templates/post-delete-hook-service-account.yaml

diff --git a/chart/scripts/post-delete-hook.sh b/chart/scripts/post-delete-hook.sh
new file mode 100755
index 000000000..6ab79bab2
--- /dev/null
+++ b/chart/scripts/post-delete-hook.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+
+set -e
+
+namespaces="${NAMESPACES}"
+timeout="${TIMEOUT}"
+ignoreTimeoutError="${IGNORETIMEOUTERROR}"
+
+if [[ -z ${namespaces} ]]; then
+  echo "No namespace is provided."
+  exit 1
+fi
+
+if [[ -z ${timeout} ]]; then
+  echo "No timeout value is provided."
+  exit 1
+fi
+
+if [[ -z ${ignoreTimeoutError} ]]; then
+  echo "No ignoreTimeoutError value is provided."
+  exit 1
+fi
+
+succeeded=()
+failed=()
+
+get_pod_count() {
+  kubectl get pods --selector app="${1}" -n "${2}" -o json | jq '.items | length'
+}
+
+echo "Uninstalling Rancher resources in the following namespaces: ${namespaces}"
+
+for namespace in ${namespaces}; do
+  for app in $(helm list -n "${namespace}" -q); do
+    if [[ ${app} =~ .crd$ ]]; then
+      echo "--- Skip the app [${app}] in the namespace [${namespace}]"
+      continue
+    fi
+    echo "--- Deleting the app [${app}] in the namespace [${namespace}]"
+    if [[ ! $(helm uninstall "${app}" -n "${namespace}") ]]; then
+      failed=("${failed[@]}" "${app}")
+      continue
+    fi
+
+    t=0
+    while true; do
+      if [[ $(get_pod_count "${app}" "${namespace}") -eq 0 ]]; then
+        echo "successfully uninstalled [${app}] in the namespace [${namespace}]"
+        succeeded=("${succeeded[@]}" "${app}")
+        break
+      fi
+      if [[ ${t} -ge ${timeout} ]]; then
+        echo "timeout uninstalling [${app}] in the namespace [${namespace}]"
+        failed=("${failed[@]}" "${app}")
+        break
+      fi
+      # by default, wait 120 seconds in total for an app to be uninstalled
+      echo "waiting 5 seconds for pods of [${app}] to be terminated ..."
+      sleep 5
+      t=$((t + 5))
+    done
+  done
+
+  # delete the helm operator pods
+  for pod in $(kubectl get pods -n "${namespace}" -o name); do
+    if [[ ${pod} =~ ^pod\/helm-operation-* ]]; then
+      echo "--- Deleting the pod [${pod}] in the namespace [${namespace}]"
+      kubectl delete "${pod}" -n "${namespace}"
+    fi
+  done
+done
+
+echo "------ Summary ------"
+if [[ ${#succeeded[@]} -ne 0 ]]; then
+  echo "Succeeded to uninstall the following apps:" "${succeeded[@]}"
+fi
+
+if [[ ${#failed[@]} -ne 0 ]]; then
+  echo "Failed to uninstall the following apps:" "${failed[@]}"
+  if [[ "${ignoreTimeoutError}" == "false" ]]; then
+    exit 2
+  fi
+else
+  echo "Cleanup finished successfully."
+fi
diff --git a/chart/templates/post-delete-hook-cluster-role-binding.yaml b/chart/templates/post-delete-hook-cluster-role-binding.yaml
new file mode 100644
index 000000000..476957b49
--- /dev/null
+++ b/chart/templates/post-delete-hook-cluster-role-binding.yaml
@@ -0,0 +1,19 @@
+{{- if .Values.postDelete.enabled }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: {{ template "rancher.fullname" . }}-post-delete
+  labels: {{ include "rancher.labels" . | nindent 4 }}
+  annotations:
+    "helm.sh/hook": post-delete
+    "helm.sh/hook-weight": "2"
+    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: {{ template "rancher.fullname" . }}-post-delete
+subjects:
+  - kind: ServiceAccount
+    name: {{ template "rancher.fullname" . }}-post-delete
+    namespace: {{ .Release.Namespace }}
+{{- end }}
diff --git a/chart/templates/post-delete-hook-cluster-role.yaml b/chart/templates/post-delete-hook-cluster-role.yaml
new file mode 100644
index 000000000..82d42467c
--- /dev/null
+++ b/chart/templates/post-delete-hook-cluster-role.yaml
@@ -0,0 +1,27 @@
+{{- if .Values.postDelete.enabled }}
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: {{ template "rancher.fullname" . }}-post-delete
+  labels: {{ include "rancher.labels" . | nindent 4 }}
+  annotations:
+    "helm.sh/hook": post-delete
+    "helm.sh/hook-weight": "1"
+    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed
+rules:
+  - apiGroups: [ "extensions","apps" ]
+    resources: [ "deployments" ]
+    verbs: [ "get", "list", "delete" ]
+  - apiGroups: [ "rbac.authorization.k8s.io" ]
+    resources: [ "clusterroles", "clusterrolebindings", "roles", "rolebindings" ]
+    verbs: [ "get", "list", "delete" ]
+  - apiGroups: [ "" ]
+    resources: [ "serviceaccounts", "pods", "secrets", "services", "configmaps" ]
+    verbs: [ "get", "list", "delete" ]
+  - apiGroups: [ "networking.k8s.io" ]
+    resources: [ "networkpolicies" ]
+    verbs: [ "get", "list", "delete" ]
+  - apiGroups: [ "admissionregistration.k8s.io" ]
+    resources: [ "validatingwebhookconfigurations" ]
+    verbs: [ "get", "list", "delete" ]
+{{- end }}
diff --git a/chart/templates/post-delete-hook-config-map.yaml b/chart/templates/post-delete-hook-config-map.yaml
new file mode 100644
index 000000000..eb7b9e66d
--- /dev/null
+++ b/chart/templates/post-delete-hook-config-map.yaml
@@ -0,0 +1,15 @@
+{{- if .Values.postDelete.enabled }}
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ template "rancher.fullname" . }}-post-delete
+  namespace: {{ .Release.Namespace }}
+  labels: {{ include "rancher.labels" . | nindent 4 }}
+  annotations:
+    "helm.sh/hook": post-delete
+    "helm.sh/hook-weight": "1"
+    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed
+data:
+  post-delete-hook.sh: |-
+{{ $.Files.Get "scripts/post-delete-hook.sh" | indent 4 }}
+{{- end }}
diff --git a/chart/templates/post-delete-hook-job.yaml b/chart/templates/post-delete-hook-job.yaml
new file mode 100644
index 000000000..661a74757
--- /dev/null
+++ b/chart/templates/post-delete-hook-job.yaml
@@ -0,0 +1,44 @@
+{{- if .Values.postDelete.enabled }}
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: {{ template "rancher.fullname" . }}-post-delete
+  namespace: {{ .Release.Namespace }}
+  labels: {{ include "rancher.labels" . | nindent 4 }}
+  annotations:
+    "helm.sh/hook": post-delete
+    "helm.sh/hook-weight": "3"
+    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+spec:
+  backoffLimit: 3
+  template:
+    metadata:
+      name: {{ template "rancher.fullname" . }}-post-delete
+      labels: {{ include "rancher.labels" . | nindent 8 }}
+    spec:
+      serviceAccountName: {{ template "rancher.fullname" . }}-post-delete
+      restartPolicy: OnFailure
+      containers:
+        - name: {{ template "rancher.name" . }}-post-delete
+          image: "{{ .Values.systemDefaultRegistry }}{{ .Values.postDelete.image.repository }}:{{ .Values.postDelete.image.tag }}"
+          imagePullPolicy: IfNotPresent
+          securityContext:
+            runAsUser: 0
+          command:
+            - /scripts/post-delete-hook.sh
+          volumeMounts:
+            - mountPath: /scripts
+              name: config-volume
+          env:
+            - name: NAMESPACES
+              value: {{ .Values.postDelete.namespaceList | join " " | quote }}
+            - name: TIMEOUT
+              value: {{ .Values.postDelete.timeout | quote }}
+            - name: IGNORETIMEOUTERROR
+              value: {{ .Values.postDelete.ignoreTimeoutError | quote }}
+      volumes:
+        - name: config-volume
+          configMap:
+            name: {{ template "rancher.fullname" . }}-post-delete
+            defaultMode: 0777
+{{- end }}
diff --git a/chart/templates/post-delete-hook-service-account.yaml b/chart/templates/post-delete-hook-service-account.yaml
new file mode 100644
index 000000000..923687d60
--- /dev/null
+++ b/chart/templates/post-delete-hook-service-account.yaml
@@ -0,0 +1,12 @@
+{{- if .Values.postDelete.enabled }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ template "rancher.fullname" . }}-post-delete
+  namespace: {{ .Release.Namespace }}
+  labels: {{ include "rancher.labels" . | nindent 4 }}
+  annotations:
+    "helm.sh/hook": post-delete
+    "helm.sh/hook-weight": "1"
+    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed
+{{- end }}
diff --git a/chart/values.yaml b/chart/values.yaml
index 548b09054..7eda1d6b7 100644
--- a/chart/values.yaml
+++ b/chart/values.yaml
@@ -137,3 +137,18 @@ customLogos:
   # storageClass: "-"
   accessMode: ReadWriteOnce
   size: 1Gi
+
+# Rancher post-delete hook
+postDelete:
+  enabled: true
+  image:
+    repository: %POST_DELETE_IMAGE_NAME%
+    tag: %POST_DELETE_IMAGE_TAG%
+  namespaceList:
+    - fleet-system
+    - cattle-system
+    - rancher-operator-system
+  # Number of seconds to wait for an app to be uninstalled
+  timeout: 120
+  # by default, the job will fail if it fail to uninstall any of the apps
+  ignoreTimeoutError: false
diff --git a/scripts/chart/build b/scripts/chart/build
index 188c4cef1..46267b606 100755
--- a/scripts/chart/build
+++ b/scripts/chart/build
@@ -14,3 +14,18 @@ cp -rf ${1} build/chart/rancher
 
 sed -i -e "s/%VERSION%/${CHART_VERSION}/g" build/chart/rancher/Chart.yaml
 sed -i -e "s/%APP_VERSION%/${VERSION}/g" build/chart/rancher/Chart.yaml
+
+# get the value of shell-image, such as rancher/shell:v0.1.6, from the file pkg/settings/setting.go
+post_delete_base=$(grep -i shell-image pkg/settings/setting.go | cut -d "," -f 2 | sed -e 's/"//g' | sed -e 's/)//g' | sed -e 's/ //g') || ""
+post_delete_image_name=$(echo "${post_delete_base}" | cut -d ":" -f 1) || ""
+post_delete_image_tag=$(echo "${post_delete_base}" | cut -d ":" -f 2) || ""
+if [[ ! ${post_delete_image_name} =~ ^rancher\/.+ ]]; then
+  echo "The image name [$post_delete_image_name] is invalid. Its prefix should be rancher/"
+  exit 1
+fi
+if [[ ! ${post_delete_image_tag} =~ ^v.+ ]]; then
+  echo "The image tag [$post_delete_image_tag] is invalid. It should start with the letter v"
+  exit 1
+fi
+sed -i -e "s@%POST_DELETE_IMAGE_NAME%@${post_delete_image_name}@g" build/chart/rancher/values.yaml
+sed -i -e "s/%POST_DELETE_IMAGE_TAG%/${post_delete_image_tag}/g" build/chart/rancher/values.yaml
-- 
GitLab