verify-shellcheck.sh 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #!/usr/bin/env bash
  2. # Copyright 2018 The Kubernetes 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. # This script lints each shell script by `shellcheck`.
  16. # Usage: `hack/verify-shellcheck.sh`.
  17. set -o errexit
  18. set -o nounset
  19. set -o pipefail
  20. KUBE_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
  21. source "${KUBE_ROOT}/hack/lib/init.sh"
  22. source "${KUBE_ROOT}/hack/lib/util.sh"
  23. # required version for this script, if not installed on the host we will
  24. # use the official docker image instead. keep this in sync with SHELLCHECK_IMAGE
  25. SHELLCHECK_VERSION="0.7.0"
  26. # upstream shellcheck latest stable image as of October 23rd, 2019
  27. SHELLCHECK_IMAGE="koalaman/shellcheck-alpine:v0.7.0@sha256:24bbf52aae6eaa27accc9f61de32d30a1498555e6ef452966d0702ff06f38ecb"
  28. # fixed name for the shellcheck docker container so we can reliably clean it up
  29. SHELLCHECK_CONTAINER="k8s-shellcheck"
  30. # disabled lints
  31. disabled=(
  32. # this lint disallows non-constant source, which we use extensively without
  33. # any known bugs
  34. 1090
  35. # this lint prefers command -v to which, they are not the same
  36. 2230
  37. )
  38. # comma separate for passing to shellcheck
  39. join_by() {
  40. local IFS="$1";
  41. shift;
  42. echo "$*";
  43. }
  44. SHELLCHECK_DISABLED="$(join_by , "${disabled[@]}")"
  45. readonly SHELLCHECK_DISABLED
  46. # creates the shellcheck container for later use
  47. create_container () {
  48. # TODO(bentheelder): this is a performance hack, we create the container with
  49. # a sleep MAX_INT32 so that it is effectively paused.
  50. # We then repeatedly exec to it to run each shellcheck, and later rm it when
  51. # we're done.
  52. # This is incredibly much faster than creating a container for each shellcheck
  53. # call ...
  54. docker run --name "${SHELLCHECK_CONTAINER}" -d --rm -v "${KUBE_ROOT}:${KUBE_ROOT}" -w "${KUBE_ROOT}" --entrypoint="sleep" "${SHELLCHECK_IMAGE}" 2147483647
  55. }
  56. # removes the shellcheck container
  57. remove_container () {
  58. docker rm -f "${SHELLCHECK_CONTAINER}" &> /dev/null || true
  59. }
  60. # ensure we're linting the k8s source tree
  61. cd "${KUBE_ROOT}"
  62. # Find all shell scripts excluding:
  63. # - Anything git-ignored - No need to lint untracked files.
  64. # - ./_* - No need to lint output directories.
  65. # - ./.git/* - Ignore anything in the git object store.
  66. # - ./vendor* - Vendored code should be fixed upstream instead.
  67. # - ./third_party/*, but re-include ./third_party/forked/* - only code we
  68. # forked should be linted and fixed.
  69. all_shell_scripts=()
  70. while IFS=$'\n' read -r script;
  71. do git check-ignore -q "$script" || all_shell_scripts+=("$script");
  72. done < <(find . -name "*.sh" \
  73. -not \( \
  74. -path ./_\* -o \
  75. -path ./.git\* -o \
  76. -path ./vendor\* -o \
  77. \( -path ./third_party\* -a -not -path ./third_party/forked\* \) \
  78. \))
  79. # make sure known failures are sorted
  80. failure_file="${KUBE_ROOT}/hack/.shellcheck_failures"
  81. kube::util::check-file-in-alphabetical-order "${failure_file}"
  82. # load known failure files
  83. failing_files=()
  84. while IFS=$'\n' read -r script;
  85. do failing_files+=("$script");
  86. done < <(cat "${failure_file}")
  87. # detect if the host machine has the required shellcheck version installed
  88. # if so, we will use that instead.
  89. HAVE_SHELLCHECK=false
  90. if which shellcheck &>/dev/null; then
  91. detected_version="$(shellcheck --version | grep 'version: .*')"
  92. if [[ "${detected_version}" = "version: ${SHELLCHECK_VERSION}" ]]; then
  93. HAVE_SHELLCHECK=true
  94. fi
  95. fi
  96. # tell the user which we've selected and possibly set up the container
  97. if ${HAVE_SHELLCHECK}; then
  98. echo "Using host shellcheck ${SHELLCHECK_VERSION} binary."
  99. else
  100. echo "Using shellcheck ${SHELLCHECK_VERSION} docker image."
  101. # remove any previous container, ensure we will attempt to cleanup on exit,
  102. # and create the container
  103. remove_container
  104. kube::util::trap_add 'remove_container' EXIT
  105. if ! output="$(create_container 2>&1)"; then
  106. {
  107. echo "Failed to create shellcheck container with output: "
  108. echo ""
  109. echo "${output}"
  110. } >&2
  111. exit 1
  112. fi
  113. fi
  114. # if KUBE_JUNIT_REPORT_DIR is set, disable colorized output.
  115. # Colorized output causes malformed XML in the JUNIT report.
  116. SHELLCHECK_COLORIZED_OUTPUT="auto"
  117. if [[ -n "${KUBE_JUNIT_REPORT_DIR:-}" ]]; then
  118. SHELLCHECK_COLORIZED_OUTPUT="never"
  119. fi
  120. # common arguments we'll pass to shellcheck
  121. SHELLCHECK_OPTIONS=(
  122. # allow following sourced files that are not specified in the command,
  123. # we need this because we specify one file at at time in order to trivially
  124. # detect which files are failing
  125. "--external-sources"
  126. # include our disabled lints
  127. "--exclude=${SHELLCHECK_DISABLED}"
  128. # set colorized output
  129. "--color=${SHELLCHECK_COLORIZED_OUTPUT}"
  130. )
  131. # lint each script, tracking failures
  132. errors=()
  133. not_failing=()
  134. for f in "${all_shell_scripts[@]}"; do
  135. set +o errexit
  136. if ${HAVE_SHELLCHECK}; then
  137. failedLint=$(shellcheck "${SHELLCHECK_OPTIONS[@]}" "${f}")
  138. else
  139. failedLint=$(docker exec -t ${SHELLCHECK_CONTAINER} \
  140. shellcheck "${SHELLCHECK_OPTIONS[@]}" "${f}")
  141. fi
  142. set -o errexit
  143. kube::util::array_contains "${f}" "${failing_files[@]}" && in_failing=$? || in_failing=$?
  144. if [[ -n "${failedLint}" ]] && [[ "${in_failing}" -ne "0" ]]; then
  145. errors+=( "${failedLint}" )
  146. fi
  147. if [[ -z "${failedLint}" ]] && [[ "${in_failing}" -eq "0" ]]; then
  148. not_failing+=( "${f}" )
  149. fi
  150. done
  151. # Check to be sure all the files that should pass lint are.
  152. if [ ${#errors[@]} -eq 0 ]; then
  153. echo 'Congratulations! All shell files are passing lint (excluding those in hack/.shellcheck_failures).'
  154. else
  155. {
  156. echo "Errors from shellcheck:"
  157. for err in "${errors[@]}"; do
  158. echo "$err"
  159. done
  160. echo
  161. echo 'Please review the above warnings. You can test via "./hack/verify-shellcheck.sh"'
  162. echo 'If the above warnings do not make sense, you can exempt this package from shellcheck'
  163. echo 'checking by adding it to hack/.shellcheck_failures (if your reviewer is okay with it).'
  164. echo
  165. } >&2
  166. exit 1
  167. fi
  168. if [[ ${#not_failing[@]} -gt 0 ]]; then
  169. {
  170. echo "Some files in hack/.shellcheck_failures are passing shellcheck. Please remove them."
  171. echo
  172. for f in "${not_failing[@]}"; do
  173. echo " $f"
  174. done
  175. echo
  176. } >&2
  177. exit 1
  178. fi
  179. # Check that all failing_files actually still exist
  180. gone=()
  181. for f in "${failing_files[@]}"; do
  182. kube::util::array_contains "$f" "${all_shell_scripts[@]}" || gone+=( "$f" )
  183. done
  184. if [[ ${#gone[@]} -gt 0 ]]; then
  185. {
  186. echo "Some files in hack/.shellcheck_failures do not exist anymore. Please remove them."
  187. echo
  188. for f in "${gone[@]}"; do
  189. echo " $f"
  190. done
  191. echo
  192. } >&2
  193. exit 1
  194. fi