[MOLECULE] Enable idempotency test in play-infrastructure
[oom/offline-installer.git] / tools / helm-healer.sh
1 #!/bin/bash
2
3 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
4
5 #
6 # globals and defaults
7 #
8
9 NAMESPACE=
10 OVERRIDES=
11 HELM_CHART_RELEASE_NAME=
12 HELM_DELETE_ALL=
13 HELM_SKIP_DEPLOY=
14 VOLUME_STORAGE=
15 HELM_TIMEOUT=3600
16 RELEASE_PREFIX=onap
17
18 #
19 # control variables
20 #
21
22 CMD=$(basename "$0")
23 COLOR_ON_RED='\033[0;31;1m'
24 COLOR_ON_GREEN='\033[0;32;1m'
25 COLOR_OFF='\033[0m'
26
27
28 #
29 # functions
30 #
31
32 help()
33 {
34 cat <<EOF
35 ${CMD} - simple tool for fixing onap helm deployment
36
37 DESCRIPTION
38     This script does nothing smart or special it just tries to
39     redeploy onap component. It can fix only problems related to
40     race conditions or timeouts. Nothing else. It will not fix
41     broken ONAP - there is no such ambition - that effort should
42     be directed in the upstream.
43
44 USAGE
45     ${CMD} -h|--help
46         This help
47
48     ${CMD} -n|--namespace <namespace>
49            (-f|--file <override>)...
50            (-s|--storage <directory>)|--no-storage-deletion
51            [-p|--release-prefix <release prefix>]
52            [-t|--timeout <secs>]
53            [(-c|--component <component release name>)...|
54             (-D|--delete-all)]
55            [-C|--clean-only]
56
57 EXAMPLES
58
59     Usage 1: (simple heuristics - redeploy failed components):
60         ${CMD} -n onap -f /some/override1.yml -s /dockerdata-nfs
61
62     Usage 2: (redeploy ONLY explicitly listed components):
63         ${CMD} -n onap -f /some/override1.yml -s /dockerdata-nfs \\
64                -c onap-aaf -c onap-sdc -c onap-portal
65
66     Usage 3: (delete EVERYTHING and redeploy):
67         ${CMD} -n onap -f /some/override1.yml -s /dockerdata-nfs --delete-all
68
69     Usage 4: (delete EVERYTHING and DO NOT redeploy - clean env.)
70         ${CMD} -n onap -s /dockerdata-nfs --delete-all --clean-only
71
72 NOTES
73
74     Namespace argument (always) and at least one override file (if you don't
75     use '--delete-all') are mandatory for this script to execute. Also you must
76     provide path to the storage ('--storage') OR explicitly request to not
77     delete file storage of the component ('--no-storage-deletion').
78
79     The storage should be a directory where persistent volume resides. It will
80     work only if the component created the persistent volume with the same
81     filename as its release name. Otherwise no files are deleted. The exception
82     is when '--delete-all' is used - in that case all content of the storage is
83     deleted (because ONAP is not consistent with the volume directory names
84     - e.g.: sdnc).
85
86     '--file' can be used multiple of times and it is used for override files
87     which are passed on to helm. The order is significant because if two
88     override files modify one value the latest one is used. This option is
89     ignored if '--clean-only' is used.
90
91     CAUTION 1: filename of an override file cannot contain whitespace! This is
92     actually helm/onap deploy plugin issue which does not handle such files. So
93     I dropped the more complicated version of this script when there is no
94     reason to support something on what will helm deploy choke anyway.
95
96     '--prefix' option is helm release argument - it is actually prefix when you
97     list the helm releases - helm is little confusing here.
98
99     CAUTION 2: By default release prefix is 'onap' - if you deployed release
100     'onap' and now run this script with different prefix then it will skip all
101     'onap-*' components and will deploy a new release with new prefix - BEWARE
102     TO USE PROPER RELEASE PREFIX!
103
104     Timeout sets the waiting time for helm deploy per component.
105
106     '--component' references to the release name of the chart which you want to
107     redeploy excplicitly - otherwise 'ALL FAILED' components will be
108     redeployed. You can target more than one component at once - just use the
109     argument multiple times.
110
111     Component option is mutually exclusive with the '--delete-all' which will
112     delete all components - healthy or not. Actually it will delete the whole
113     NAMESPACE and everything in it. Also to be sure it will cleanup all
114     orphaned images and volumes on all kubernetes nodes.
115
116     '--clean-only' can be used with any usage: heuristics, explicit component
117     list or with '--delete-all'. It basically just skips the last step - the
118     actual redeploy.
119 EOF
120 }
121
122 use_help()
123 {
124     printf "Try help: ${CMD} --help\n"
125 }
126
127 msg()
128 {
129     printf "${COLOR_ON_GREEN}INFO: $@ ${COLOR_OFF}\n"
130 }
131
132 error()
133 {
134     printf "${COLOR_ON_RED}ERROR: $@ ${COLOR_OFF}\n"
135 }
136
137 on_exit()
138 {
139     printf "$COLOR_OFF"
140 }
141
142 # remove all successfully completed jobs
143 clean_jobs()
144 {
145     kubectl get jobs -n ${NAMESPACE} \
146         --ignore-not-found=true \
147         --no-headers=true | \
148         while read -r _job _completion _duration _age ; do
149             _done=$(echo ${_completion} | awk 'BEGIN {FS="/";} {print $1;}')
150             _desired=$(echo ${_completion} | awk 'BEGIN {FS="/";} {print $2;}')
151             if [ "$_desired" -eq "$_done" ] ; then
152                 delete_job "$_job"
153             fi
154         done
155 }
156
157 get_failed_labels()
158 {
159     get_labels 'status.phase==Failed'
160 }
161
162 # arg: [optional: selector]
163 get_labels()
164 {
165     if [ -n "$1" ] ; then
166         _selector="--field-selector=${1}"
167     else
168         _selector=
169     fi
170
171     kubectl get pods -n ${NAMESPACE} \
172         --show-labels=true \
173         ${_selector} \
174         --ignore-not-found=true \
175         --no-headers=true | \
176         while read -r _pod _ready _status _restart _age _labels ; do
177             [ -z "$_labels" ] && break
178             for _label in $(echo "$_labels" | tr ',' ' ') ; do
179                 case "$_label" in
180                     release=*)
181                         _label=$(echo "$_label" | sed 's/release=//')
182                         echo "$_label"
183                         ;;
184                 esac
185             done
186         done | sort -u
187 }
188
189 # arg: <release name>
190 helm_undeploy()
191 {
192     msg "Undeploy helm release name: ${1}"
193     # Helm v3 does not support "--purge" flag since it's a default behavior for v3
194     if [[ $(helm version --template "{{.Version}}") =~ ^v3 ]];then
195         helm undeploy ${1}
196     else
197         helm undeploy ${1} --purge
198     fi
199     sleep 15s
200 }
201
202 # arg: <job name>
203 delete_job()
204 {
205     kubectl delete job -n ${NAMESPACE} \
206         --cascade=true \
207         --now=true \
208         --wait=true \
209         ${1}
210
211     # wait for job to be deleted
212     _output=start
213     while [ -n "$_output" ] && sleep 1 ; do
214         _output=$(kubectl get pods -n ${NAMESPACE} \
215             --ignore-not-found=true \
216             --no-headers=true \
217             --selector="job-name=${1}")
218     done
219 }
220
221 #arg: <component>
222 get_resources_for_component()
223 {
224
225 helm status $1 | awk -f <(cat - <<-'EOD'
226 BEGIN {
227   work="no"
228   kind=""
229   a["dummy"]=""
230 }
231
232 $1 ~ ":" {
233   if ( $1 == "RESOURCES:" ) {
234           work="yes"
235 } else {
236   work="no"
237 }
238
239 }
240
241 $1 == "==>" {
242   split($2, a, "[/(]")
243   kind=a[2]
244 }
245
246 $1 != "NAME" && $1 != "==>" && work == "yes" && $1 !~ ":" && $1 != "" {
247   printf "%s/%s\n", kind, $1
248 }
249
250 EOD
251 )
252 }
253
254 # arg: <resource>
255 delete_resource()
256 {
257     local _resource="$1"
258     local _kind="${_resource%/*}"
259     local _name="${_resource#*/}"
260
261
262     if kubectl get ${_resource} >/dev/null 2>&1; then
263         msg "${_resource} has not been removed with helm undeploy, manual removal is required. Proceeding"
264         kubectl delete ${_resource} -n ${NAMESPACE} \
265             --cascade=true \
266             --now=true \
267             --wait=true \
268             2>&1 | grep -iv 'not[[:space:]]*found'
269
270         # wait for resource to be deleted
271         _output=start
272         while [ -n "$_output" ] && sleep 1 ; do
273             _output=$(kubectl get ${_kind} ${_name} -n ${NAMESPACE} \
274                 --ignore-not-found=true \
275                 --no-headers=true )
276         done
277         msg "Done"
278     fi
279 }
280
281 delete_namespace()
282 {
283     msg "Delete the whole namespace: ${NAMESPACE}"
284     kubectl delete namespace \
285         --cascade=true \
286         --now=true \
287         --wait=true \
288         "$NAMESPACE"
289
290     # wait for namespace to be deleted
291     _output=start
292     while [ -n "$_output" ] && sleep 1 ; do
293         _output=$(kubectl get all -n ${NAMESPACE} \
294             --ignore-not-found=true \
295             --no-headers=true)
296     done
297 }
298
299 delete_persistent_volume()
300 {
301     _persistent_volume=$1
302      if kubectl get ${_persistent_volume} >/dev/null 2>&1; then
303           msg "${_persistent_volume} has not been removed with helm undeploy, manual removal is required. Proceeding"
304           #very often k8s hangs on Terminating state for pv due to  still active pvc. It is better to delete pvc directly
305          _claim=$(kubectl get ${_persistent_volume} -o jsonpath='{ .spec.claimRef.name}')
306          delete_resource PersistentVolumeClaim/${_claim}
307      fi
308 }
309
310 # arg: [optional: directory]
311 delete_storage()
312 {
313     _node=$(kubectl get nodes \
314         --selector=node-role.kubernetes.io/worker \
315         -o wide \
316         --no-headers=true | \
317         awk '{print $6}' | head -n 1)
318
319     if [ -z "$_node" ] ; then
320         error "Could not list kubernetes nodes - SKIPPING DELETION"
321     else
322         if [ -n "$1" ] ; then
323             msg "Delete directory '${1}' on $_node"
324             ssh $_node "rm -rf '${1}'"
325         else
326             msg "Delete directories '${VOLUME_STORAGE}/*' on $_node"
327             ssh $_node "find '${VOLUME_STORAGE}' -maxdepth 1 -mindepth 1 -exec rm -rf '{}' \;"
328         fi
329     fi
330 }
331
332 docker_cleanup()
333 {
334     _nodes=$(kubectl get nodes \
335         --selector=node-role.kubernetes.io/worker \
336         -o wide \
337         --no-headers=true | \
338         awk '{print $6}')
339
340     if [ -z "$_nodes" ] ; then
341         error "Could not list kubernetes nodes - SKIPPING docker cleanup"
342         return
343     fi
344
345     for _node in $_nodes ; do
346         msg "Docker cleanup on $_node"
347         ssh $_node "docker system prune --force --all --volumes" >/dev/null &
348     done
349
350     msg "We are waiting now for docker cleanup to finish on all nodes..."
351     wait
352 }
353
354 is_helm_serve_running()
355 {
356     # healthy result: HTTP/1.1 200 OK
357     _helm_serve_result=$(curl -w %{http_code} --silent --connect-timeout 3 http://127.0.0.1:8879/ -o /dev/null)
358
359     if [ "$_helm_serve_result" == "200" ] ; then
360         return 0
361     else
362         return 1
363     fi
364 }
365
366 # arg: <release name>
367 undeploy_component()
368 {
369     local _component=$1
370
371     #Because Helm undeploy is not reliable: Gathering resources assigned to componen to track and remove orphans later
372     _component_resources=($(get_resources_for_component ${_component}))
373
374     declare -a _persistent_volumes
375     declare -a _standard
376     declare -a _unknown_kinds
377
378     for resource in ${_component_resources[@]}; do
379         case $resource in
380             CronJob/* | Job/* | Secret/* | ConfigMap/* | Pod/* | Service/* | Deployment/* | StatefulSet/*)
381                 _standard+=(${resource});;
382             #Ignoring PVC, they will be handled along with PV as 'helm' status does not return them for some components
383             PersistentVolumeClaim/*)
384                 ;;
385             PersistentVolume/*)
386                 _persistent_volumes+=(${resource});;
387             *)
388                 _unknown_kinds+=(${resource})
389         esac
390     done
391
392
393     #Gathering physical location of directories for persistent volumes to delete them after undeploy
394     declare -a _physical_locations
395     for volume in ${_persistent_volumes[@]}; do
396         _physical_locations+=($(kubectl get ${volume} -o jsonpath='{ .spec.hostPath.path}' ))
397     done
398
399     helm_undeploy ${_component}
400
401     #Manual items removal
402     for resource in ${_standard[@]}; do
403         delete_resource ${resource}
404     done
405
406     for volume in ${_persistent_volumes[@]}; do
407         delete_persistent_volume ${volume}
408     done
409
410     for subdir in ${_physical_locations[@]}; do
411         delete_storage ${subdir}
412     done
413
414     if [ "${#_unknown_kinds[@]}" -ne 0 ] ; then
415         for resource in ${_unknown_kinds[@]}; do
416             error "Untracked resource kind present: ${resource}, attempting to delete it..."
417             delete_resource ${resource}
418         done
419         return
420     fi
421 }
422
423 # arg: <release name>
424 deploy_component()
425 {
426     # TODO: until I can verify that this does the same for this component as helm deploy
427     #msg "Redeployment of the component ${1}..."
428     #helm install "local/${_chart}" --name ${1} --namespace ${NAMESPACE} --wait --timeout ${HELM_TIMEOUT}
429     error "NOT IMPLEMENTED"
430 }
431
432
433 #
434 # arguments
435 #
436
437 state=nil
438 arg_namespace=
439 arg_overrides=
440 arg_timeout=
441 arg_storage=
442 arg_nostorage=
443 arg_components=
444 arg_prefix=
445 arg_deleteall=
446 arg_cleanonly=
447 while [ -n "$1" ] ; do
448     case $state in
449         nil)
450             case "$1" in
451                 -h|--help)
452                     help
453                     exit 0
454                     ;;
455                 -n|--namespace)
456                     state=namespace
457                     ;;
458                 -f|--file)
459                     state=override
460                     ;;
461                 -t|--timeout)
462                     state=timeout
463                     ;;
464                 -s|--storage)
465                     state=storage
466                     ;;
467                 --no-storage-deletion)
468                     if [ -n "$arg_storage" ] ; then
469                         error "Usage of storage argument together with no storage deletion option!"
470                         use_help
471                         exit 1
472                     elif [ -z "$arg_nostorage" ] ; then
473                         arg_nostorage=nostorage
474                     else
475                         error "Duplicit argument for no storage option! (IGNORING)"
476                     fi
477                     ;;
478                 -c|--component)
479                     if [ -n "$arg_deleteall" ] ; then
480                         error "'Delete all components' used already - argument mismatch"
481                         use_help
482                         exit 1
483                     fi
484                     state=component
485                     ;;
486                 -D|--delete-all)
487                     if [ -n "$arg_components" ] ; then
488                         error "Explicit component(s) provided already - argument mismatch"
489                         use_help
490                         exit 1
491                     elif [ -z "$arg_deleteall" ] ; then
492                         arg_deleteall=deleteall
493                     else
494                         error "Duplicit argument for 'delete all' option! (IGNORING)"
495                     fi
496                     ;;
497                 -p|--prefix)
498                     state=prefix
499                     ;;
500                 -C|--clean-only)
501                     if [ -z "$arg_cleanonly" ] ; then
502                         arg_cleanonly=cleanonly
503                     else
504                         error "Duplicit argument for 'clean only' option! (IGNORING)"
505                     fi
506                     ;;
507                 *)
508                     error "Unknown parameter: $1"
509                     use_help
510                     exit 1
511                     ;;
512             esac
513             ;;
514         namespace)
515             if [ -z "$arg_namespace" ] ; then
516                 arg_namespace="$1"
517                 state=nil
518             else
519                 error "Duplicit argument for namespace!"
520                 use_help
521                 exit 1
522             fi
523             ;;
524         override)
525             if ! [ -f "$1" ] ; then
526                 error "Wrong filename for override file: $1"
527                 use_help
528                 exit 1
529             fi
530             arg_overrides="${arg_overrides} -f $1"
531             state=nil
532             ;;
533         component)
534             arg_components="${arg_components} $1"
535             state=nil
536             ;;
537         prefix)
538             if [ -z "$arg_prefix" ] ; then
539                 arg_prefix="$1"
540                 state=nil
541             else
542                 error "Duplicit argument for release prefix!"
543                 use_help
544                 exit 1
545             fi
546             ;;
547         timeout)
548             if [ -z "$arg_timeout" ] ; then
549                 if ! echo "$1" | grep -q '^[0-9]\+$' ; then
550                     error "Timeout must be an integer: $1"
551                     use_help
552                     exit 1
553                 fi
554                 arg_timeout="$1"
555                 state=nil
556             else
557                 error "Duplicit argument for timeout!"
558                 use_help
559                 exit 1
560             fi
561             ;;
562         storage)
563             if [ -n "$arg_nostorage" ] ; then
564                 error "Usage of storage argument together with no storage deletion option!"
565                 use_help
566                 exit 1
567             elif [ -z "$arg_storage" ] ; then
568                 arg_storage="$1"
569                 state=nil
570             else
571                 error "Duplicit argument for storage!"
572                 use_help
573                 exit 1
574             fi
575             ;;
576     esac
577     shift
578 done
579
580 # sanity checks
581
582 if [ -z "$arg_namespace" ] ; then
583     error "Missing namespace"
584     use_help
585     exit 1
586 else
587     NAMESPACE="$arg_namespace"
588 fi
589
590 if [ -z "$arg_overrides" ] && [ -z "$arg_cleanonly" ] ; then
591     error "Missing override file(s) or use '--clean-only'"
592     use_help
593     exit 1
594 else
595     OVERRIDES="$arg_overrides"
596 fi
597
598 if [ -n "$arg_prefix" ] ; then
599     RELEASE_PREFIX="$arg_prefix"
600 fi
601
602 if [ -n "$arg_timeout" ] ; then
603     HELM_TIMEOUT="$arg_timeout"
604 fi
605
606 if [ -n "$arg_storage" ] ; then
607     VOLUME_STORAGE="$arg_storage"
608 elif [ -z "$arg_nostorage" ] ; then
609     error "Missing storage argument! If it is intended then use '--no-storage-deletion' option"
610     use_help
611     exit 1
612 fi
613
614 if [ -n "$arg_components" ] ; then
615     HELM_CHART_RELEASE_NAME="$arg_components"
616 fi
617
618 if [ -n "$arg_deleteall" ] ; then
619     HELM_DELETE_ALL=yes
620 fi
621
622 if [ -n "$arg_cleanonly" ] ; then
623     HELM_SKIP_DEPLOY=yes
624 fi
625
626 # If running with helm v3 a time unit has to be appended to HELM_TIMEOUT
627 if [[ $(helm version --template "{{.Version}}") =~ ^v3 ]];then
628     HELM_TIMEOUT="${HELM_TIMEOUT}s"
629 fi
630
631 #
632 # main
633 #
634
635 # set trap for this script cleanup
636 trap on_exit INT QUIT TERM EXIT
637
638 # another sanity checks
639 for tool in helm kubectl curl ; do
640     if ! which "$tool" >/dev/null 2>&1 ; then
641         error "Missing '${tool}' command"
642         exit 1
643     fi
644 done
645
646 if ! is_helm_serve_running ; then
647     error "'helm serve' is not running (http://localhost:8879)"
648     exit 1
649 fi
650
651 # if --delete-all is used then redeploy all components (the current namespace is deleted)
652 if [ -n "$HELM_DELETE_ALL" ] ; then
653     # undeploy helm release (prefix)
654     helm_undeploy "$RELEASE_PREFIX"
655
656     # we will delete the whole namespace
657     delete_namespace
658
659     # we will cleanup docker on each node
660     docker_cleanup
661
662     # we will delete the content of storage (volumes)
663     if [ -n "$VOLUME_STORAGE" ] ; then
664         delete_storage
665     fi
666 # delete and redeploy explicit or failed components...
667 else
668     # if a helm chart release name was given then just redeploy said component and quit
669     if [ -n "$HELM_CHART_RELEASE_NAME" ] ; then
670         msg "Explicitly asked for component redeploy: ${HELM_CHART_RELEASE_NAME}"
671         _COMPONENTS="$HELM_CHART_RELEASE_NAME"
672     # simple heuristics: redeploy only failed components
673     else
674         msg "Delete successfully completed jobs..."
675         clean_jobs
676
677         msg "Find failed components..."
678         _COMPONENTS=$(get_failed_labels)
679     fi
680
681     for _component in ${_COMPONENTS} ; do
682         if echo "$_component" | grep -q "^${RELEASE_PREFIX}-" ; then
683             msg "Redeploy component: ${_component}"
684             undeploy_component ${_component}
685         else
686             error "Component release name '${_component}' does not match release prefix: ${RELEASE_PREFIX} (SKIP)"
687         fi
688     done
689 fi
690
691 if [ -z "$HELM_SKIP_DEPLOY" ] ; then
692     # TODO: this is suboptimal - find a way how to deploy only the affected component...
693     msg "Redeploy onap..."
694     msg helm deploy ${RELEASE_PREFIX} local/onap --namespace ${NAMESPACE} ${OVERRIDES} --timeout ${HELM_TIMEOUT}
695     helm deploy ${RELEASE_PREFIX} local/onap --namespace ${NAMESPACE} ${OVERRIDES} --timeout ${HELM_TIMEOUT}
696 else
697     msg "Clean only option used: Skipping redeploy..."
698 fi
699
700 msg DONE
701
702 exit $?
703