verify-shellcheck.sh 6.8 KB

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