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