#!/usr/bin/env bash # Copyright (c) 2017-2023 Intel Corporation # # SPDX-License-Identifier: Apache-2.0 # # Description: Central script to run all static checks. # This script should be called by all other repositories to ensure # there is only a single source of all static checks. set -e [[ -n "${DEBUG}" ]] && set -x cidir=$(realpath "$(dirname "$0")") # shellcheck source=/dev/null source "${cidir}/common.bash" # By default in Golang >= 1.16 GO111MODULE is set to "on", # some subprojects in this repo may not support "go modules", # set GO111MODULE to "auto" to enable module-aware mode only when # a go.mod file is present in the current directory. export GO111MODULE="auto" # List of files to delete on exit files_to_remove=() script_name=${0##*/} # Static check functions must follow the following naming conventions: # # All static check function names must match this pattern. typeset -r check_func_regex="^static_check_" # All architecture-specific static check functions must match this pattern. typeset -r arch_func_regex="_arch_specific$" repo="" repo_path="" specific_branch="false" force="false" branch=${branch:-main} # Which static check functions to consider. handle_funcs="all" single_func_only="false" list_only="false" # number of seconds to wait for curl to check a URL typeset url_check_timeout_secs="${url_check_timeout_secs:-60}" # number of attempts that will be made to check an individual URL. typeset url_check_max_tries="${url_check_max_tries:-3}" typeset -A long_options # Generated code ignore_clh_generated_code="virtcontainers/pkg/cloud-hypervisor/client" paths_to_skip=( "${ignore_clh_generated_code}" "vendor" ) # Skip paths that are not statically checked # $1 : List of paths to check, space separated list # If you have a list in a bash array call in this way: # list=$(skip_paths "${list[@]}") # If you still want to use it as an array do: # list=(${list}) skip_paths(){ local list_param="${1}" [[ -z "${list_param}" ]] && return local list read -r -a list <<< "${list_param}" for p in "${paths_to_skip[@]}"; do new_list=() for l in "${list[@]}"; do if echo "${l}" | grep -qv "${p}"; then new_list=("${new_list[@]}" "${l}") fi done list=("${new_list[@]}") done echo "${list[@]}" } long_options=( [all]="Force checking of all changes, including files in the base branch" [branch]="Specify upstream branch to compare against (default '${branch}')" [docs]="Check document files" [dockerfiles]="Check dockerfiles" [files]="Check files" [force]="Force a skipped test to run" [golang]="Check '.go' files" [help]="Display usage statement" [json]="Check JSON files" [labels]="Check labels databases" [licenses]="Check licenses" [list]="List tests that would run" [no-arch]="Run/list all tests except architecture-specific ones" [only-arch]="Only run/list architecture-specific tests" [repo:]="Specify GitHub URL of repo to use (github.com/user/repo)" [repo-path:]="Specify path to repository to check (default: \$GOPATH/src/\$repo)" [scripts]="Check script files" [versions]="Check versions files" [xml]="Check XML files" ) yamllint_cmd="yamllint" have_yamllint_cmd=$(command -v "${yamllint_cmd}" || true) chronic=chronic # Disable chronic on OSX to avoid having to update the Travis config files # for additional packages on that platform. [[ "$(uname -s)" == "Darwin" ]] && chronic= usage() { cat </dev/null || die "function '${name}' does not exist" } # Calls die() if the specified function is not valid or not a check function. ensure_func_is_check_func() { local name="$1" func_is_valid "${name}" local ret { echo "${name}" | grep -q "${check_func_regex}"; ret=$?; } [[ "${ret}" = 0 ]] || die "function '${name}' is not a check function" } # Returns "yes" if the specified function needs to run on all architectures, # else "no". func_is_arch_specific() { local name="$1" ensure_func_is_check_func "${name}" local ret { echo "${name}" | grep -q "${arch_func_regex}"; ret=$?; } if [[ "${ret}" = 0 ]]; then echo "yes" else echo "no" fi } function remove_tmp_files() { rm -rf "${files_to_remove[@]}" } # Convert a golang package to a full path pkg_to_path() { local pkg="$1" go list -f '{{.Dir}}' "${pkg}" } # Check that chronic is installed, otherwise die. need_chronic() { [[ -z "${chronic}" ]] && return # shellcheck disable=SC2034 local first_word="${chronic%% *}" command -v chronic &>/dev/null || \ die "chronic command not found. You must have it installed to run this check." \ "Usually it is distributed with the 'moreutils' package of your Linux distribution." } static_check_go_arch_specific() { local go_packages local submodule_packages local all_packages pushd "${repo_path}" # List of all golang packages found in all submodules # # These will be ignored: since they are references to other # repositories, we assume they are tested independently in their # repository so do not need to be re-tested here. submodule_packages=$(mktemp) git submodule -q foreach "go list ./..." | sort > "${submodule_packages}" || true # all packages all_packages=$(mktemp) go list ./... | sort > "${all_packages}" || true files_to_remove+=("${submodule_packages}" "${all_packages}") # List of packages to consider which is defined as: # # "all packages" - "submodule packages" # # Note: the vendor filtering is required for versions of go older than 1.9 go_packages=$(comm -3 "${all_packages}" "${submodule_packages}" || true) # shellcheck disable=SC2128 go_packages=$(skip_paths "${go_packages[@]}") # No packages to test [[ -z "${go_packages}" ]] && popd && return local linter="golangci-lint" # Run golang checks if [[ ! "$(command -v "${linter}")" ]] then info "Installing ${linter}" local linter_url linter_url=$(get_test_version "languages.golangci-lint.url") local linter_version linter_version=$(get_test_version "languages.golangci-lint.version") info "Forcing ${linter} version ${linter_version}" # shellcheck disable=SC2046 curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin" "v${linter_version}" command -v "${linter}" &>/dev/null || \ die "${linter} command not found. Ensure that \"\$GOPATH/bin\" is in your \$PATH." fi local linter_args="run -c ${cidir}/.golangci.yml" # Non-option arguments other than "./..." are # considered to be directories by $linter, not package names. # Hence, we need to obtain a list of package directories to check, # excluding any that relate to submodules. local dirs for pkg in ${go_packages} do path=$(pkg_to_path "${pkg}") makefile="${path}/Makefile" # perform a basic build since some repos generate code which # is required for the package to be buildable (and thus # checkable). [[ -f "${makefile}" ]] && (cd "${path}" && make) dirs+=" ${path}" done info "Running ${linter} checks on the following packages:\n" echo "${go_packages}" echo info "Package paths:\n" echo "${dirs}" | sed 's/^ *//g' | tr ' ' '\n' for d in ${dirs};do info "Running ${linter} on ${d}" (cd "${d}" && GO111MODULE=auto eval "${linter}" "${linter_args}" ".") done popd } # Install yamllint in the different Linux distributions install_yamllint() { package="yamllint" # shellcheck disable=SC2154 case "${ID}" in centos|rhel) sudo yum -y install "${package}" ;; ubuntu) sudo apt-get -y install "${package}" ;; fedora) sudo dnf -y install "${package}" ;; *) die "Please install yamllint on ${ID}" ;; esac have_yamllint_cmd=$(command -v "${yamllint_cmd}" || true) if [[ -z "${have_yamllint_cmd}" ]]; then info "Cannot install ${package}" && return fi } # Check the "versions database". # # Some repositories use a versions database to maintain version information # about non-golang dependencies. If found, check it for validity. static_check_versions() { local db="versions.yaml" if [[ -z "${have_yamllint_cmd}" ]]; then info "Installing yamllint" install_yamllint fi pushd "${repo_path}" [[ ! -e "${db}" ]] && popd && return if [[ -n "${have_yamllint_cmd}" ]]; then eval "${yamllint_cmd}" "${db}" else info "Cannot check versions as ${yamllint_cmd} not available" fi popd } static_check_labels() { [[ "$(uname -s)" != Linux ]] && info "Can only check labels under Linux" && return # Handle SLES which doesn't provide the required command. [[ -z "${have_yamllint_cmd}" ]] && info "Cannot check labels as ${yamllint_cmd} not available" && return # Since this script is called from another repositories directory, # ensure the utility is built before the script below (which uses it) is run. (cd "${test_dir}/cmd/github-labels" && make) tmp=$(mktemp) files_to_remove+=("${tmp}") info "Checking labels for repo ${repo} using temporary combined database ${tmp}" bash -f "${test_dir}/cmd/github-labels/github-labels.sh" "generate" "${repo}" "${tmp}" } # Ensure all files (where possible) contain an SPDX license header static_check_license_headers() { # The branch is the baseline - ignore it. [[ "${specific_branch}" = "true" ]] && return # See: https://spdx.org/licenses/Apache-2.0.html local -r spdx_tag="SPDX-License-Identifier" local -r spdx_license="Apache-2.0" local -r license_pattern="${spdx_tag}: ${spdx_license}" local -r copyright_pattern="Copyright" local header_checks=() header_checks+=("SPDX license header::${license_pattern}") header_checks+=("Copyright header:-i:${copyright_pattern}") pushd "${repo_path}" files=$(get_pr_changed_file_details || true) # Strip off status and convert to array mapfile -t files < <(echo "${files}"|awk '{print $NF}') text_files=() # Filter out non-text files for file in "${files[@]}"; do if [[ -f "${file}" ]] && file --mime-type "${file}" | grep -q "text/"; then text_files+=("${file}") else info "Ignoring non-text file: ${file}" fi done files=("${text_files[@]}") # no text files were changed [[ "${#files[@]}" -eq 0 ]] && info "No files found" && popd && return local header_check for header_check in "${header_checks[@]}" do local desc desc=$(echo "${header_check}"|cut -d: -f1) local extra_args extra_args=$(echo "${header_check}"|cut -d: -f2) local pattern pattern=$(echo "${header_check}"|cut -d: -f3-) info "Checking ${desc}" local missing # shellcheck disable=SC2086 missing=$(grep \ --exclude=".git/*" \ --exclude=".editorconfig" \ --exclude=".gitignore" \ --exclude=".dockerignore" \ --exclude="Gopkg.lock" \ --exclude="*.gpl.c" \ --exclude="*.ipynb" \ --exclude="*.jpg" \ --exclude="*.json" \ --exclude="LICENSE*" \ --exclude="THIRD-PARTY" \ --exclude="*.md" \ --exclude="*.pb.go" \ --exclude="*pb_test.go" \ --exclude="*.bin" \ --exclude="*.png" \ --exclude="*.pub" \ --exclude="*.service" \ --exclude="*.svg" \ --exclude="*.drawio" \ --exclude="*.toml" \ --exclude="*.txt" \ --exclude="*.dtd" \ --exclude="vendor/*" \ --exclude="VERSION" \ --exclude="kata_config_version" \ --exclude="tools/packaging/kernel/configs/*" \ --exclude="virtcontainers/pkg/firecracker/*" \ --exclude="${ignore_clh_generated_code}*" \ --exclude="*.xml" \ --exclude="*.yaml" \ --exclude="*.yml" \ --exclude="go.mod" \ --exclude="go.sum" \ --exclude="*.lock" \ --exclude="grpc-rs/*" \ --exclude="target/*" \ --exclude="*.patch" \ --exclude="*.diff" \ --exclude="tools/packaging/static-build/qemu.blacklist" \ --exclude="tools/packaging/qemu/default-configs/*" \ --exclude="src/libs/protocols/protos/gogo/*.proto" \ --exclude="src/libs/protocols/protos/google/*.proto" \ --exclude="src/libs/protocols/protos/cri-api/api.proto" \ --exclude="src/mem-agent/example/protocols/protos/google/protobuf/*.proto" \ --exclude="src/libs/*/test/texture/*" \ --exclude="*.dic" \ -EL ${extra_args} -E "\<${pattern}\>" \ "${files[@]}" || true) if [[ -n "${missing}" ]]; then cat >&2 <<-EOF ERROR: Required ${desc} check ('${pattern}') failed for the following files: ${missing} EOF exit 1 fi done popd } run_url_check_cmd() { local url="${1:-}" [[ -n "${url}" ]] || die "need URL" local out_file="${2:-}" [[ -n "${out_file}" ]] || die "need output file" # Can be blank local extra_args="${3:-}" local curl_extra_args=() curl_extra_args+=("${extra_args}") # Authenticate for github to increase threshold for rate limiting if [[ "${url}" =~ github\.com && -n "${GITHUB_USER}" && -n "${GITHUB_TOKEN}" ]]; then curl_extra_args+=("-u ${GITHUB_USER}:${GITHUB_TOKEN}") fi # Some endpoints return 403 to HEAD but 200 for GET, # so perform a GET but only read headers. # shellcheck disable=SC2048,SC2086 curl \ ${curl_extra_args[*]} \ -sIL \ -X GET \ -c - \ -H "Accept-Encoding: zstd, none, gzip, deflate" \ --max-time "${url_check_timeout_secs}" \ --retry "${url_check_max_tries}" \ "${url}" \ &>"${out_file}" } check_url() { local url="${1:-}" [[ -n "${url}" ]] || die "need URL to check" local invalid_urls_dir="${2:-}" [[ -n "${invalid_urls_dir}" ]] || die "need invalid URLs directory" local curl_out curl_out=$(mktemp) files_to_remove+=("${curl_out}") # Process specific file to avoid out-of-order writes local invalid_file invalid_file=$(printf "%s/%d" "${invalid_urls_dir}" "$$") local ret local -a errors=() local -a user_agents=() # Test an unspecified UA (curl default) user_agents+=('') # Test an explictly blank UA user_agents+=('""') # Single space user_agents+=(' ') # CLI HTTP tools user_agents+=('Wget') user_agents+=('curl') # console based browsers # Hopefully, these will always be supported for a11y. user_agents+=('Lynx') user_agents+=('Elinks') # Emacs' w3m browser user_agents+=('Emacs') # The full craziness user_agents+=('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36') local user_agent # Cycle through the user agents until we find one that works. # # Note that we also test an unspecified user agent # (no '-A '). for user_agent in "${user_agents[@]}" do info "Checking URL ${url} with User Agent '${user_agent}'" local curl_ua_args="" # shellcheck disable=SC2034 [[ -n "${user_agent}" ]] && curl_ua_args="-A '${user_agent}'" local http_statuses http_statuses=$(grep -E "^HTTP" "${curl_out}" |\ awk '{print $2}' || true) if [[ -z "${http_statuses}" ]]; then errors+=("no HTTP status codes for URL '${url}' (user agent: '${user_agent}')") continue fi local status local -i fail_count=0 # Check all HTTP status codes for status in ${http_statuses} do # Ignore the following ranges of status codes: # # - 1xx: Informational codes. # - 2xx: Success codes. # - 3xx: Redirection codes. # - 405: Specifically to handle some sites # which get upset by "curl -L" when the # redirection is not required. # # Anything else is considered an error. # # See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes { grep -qE "^(1[0-9][0-9]|2[0-9][0-9]|3[0-9][0-9]|405)" <<< "${status}"; ret=$?; } || true [[ "${ret}" -eq 0 ]] && continue fail_count+=1 done # If we didn't receive any unexpected HTTP status codes for # this UA, the URL is valid so we don't need to check with any # further UAs, so clear any (transitory) errors we've # recorded. [[ "${fail_count}" -eq 0 ]] && errors=() && break echo "${url}" >> "${invalid_file}" errors+=("found HTTP error status codes for URL ${url} (status: '${status}', user agent: '${user_agent}')") done # shellcheck disable=SC2128 [[ "${#errors}" = 0 ]] && return 0 die "failed to check URL '${url}': errors: '${errors[*]}'" } # Perform basic checks on documentation files static_check_docs() { local cmd="xurls" pushd "${repo_path}" if [[ ! "$(command -v "${cmd}")" ]] then info "Installing ${cmd} utility" local version local url version=$(get_test_version "externals.xurls.version") url=$(get_test_version "externals.xurls.url") # xurls is very fussy about how it's built. go install "${url}@${version}" # shellcheck disable=SC2016 command -v xurls &>/dev/null || die 'xurls not found. Ensure that "$GOPATH/bin" is in your $PATH' fi info "Checking documentation" local doc local all_docs local docs local docs_status local new_docs local new_urls local url pushd "${repo_path}" # shellcheck disable=SC2128 all_docs=$(git ls-files "*.md" | grep -Ev "(grpc-rs|target)/" | sort || true) all_docs=$(skip_paths "${all_docs[@]}") if [[ "${specific_branch}" = "true" ]] then info "Checking all documents in ${branch} branch" docs="${all_docs}" else info "Checking local branch for changed documents only" docs_status=$(get_pr_changed_file_details || true) docs_status=$(echo "${docs_status}" | grep "\.md$" || true) docs=$(echo "${docs_status}" | awk '{print $NF}' | sort) # shellcheck disable=SC2128 docs=$(skip_paths "${docs[@]}") # Newly-added docs new_docs=$(echo "${docs_status}" | awk '/^A/ {print $NF}' | sort) # shellcheck disable=SC2128 new_docs=$(skip_paths "${new_docs[@]}") for doc in ${new_docs} do # A new document file has been added. If that new doc # file is referenced by any files on this PR, checking # its URL will fail since the PR hasn't been merged # yet. We could construct the URL based on the users # original PR branch and validate that. But it's # simpler to just construct the URL that the "pending # document" *will* result in when the PR has landed # and then check docs for that new URL and exclude # them from the real URL check. url="https://${repo}/blob/${branch}/${doc}" new_urls+=" ${url}" done fi [[ -z "${docs}" ]] && info "No documentation to check" && return # shellcheck disable=SC2034 local urls local url_map url_map=$(mktemp) local invalid_urls invalid_urls=$(mktemp) local md_links md_links=$(mktemp) files_to_remove+=("${url_map}" "${invalid_urls}" "${md_links}") info "Checking document markdown references" local md_docs_to_check # All markdown docs are checked (not just those changed by a PR). This # is necessary to guarantee that all docs are referenced. md_docs_to_check="${all_docs}" # shellcheck disable=SC2016 command -v kata-check-markdown &>/dev/null ||\ (cd "${test_dir}/cmd/check-markdown" && make) # shellcheck disable=SC2016 command -v kata-check-markdown &>/dev/null || \ die 'kata-check-markdown command not found. Ensure that "$GOPATH/bin" is in your $PATH.' for doc in ${md_docs_to_check} do kata-check-markdown check "${doc}" # Get a link of all other markdown files this doc references kata-check-markdown list links --format tsv --no-header "${doc}" |\ grep "external-link" |\ awk '{print $3}' |\ sort -u >> "${md_links}" done # clean the list of links local tmp tmp=$(mktemp) sort -u "${md_links}" > "${tmp}" mv "${tmp}" "${md_links}" # A list of markdown files that do not have to be referenced by any # other markdown file. exclude_doc_regexs+=() exclude_doc_regexs+=(^CODE_OF_CONDUCT\.md$) exclude_doc_regexs+=(^CONTRIBUTING\.md$) exclude_doc_regexs+=(^SECURITY\.md$) # Magic github template files exclude_doc_regexs+=(^\.github/.*\.md$) # The top level README doesn't need to be referenced by any other # since it displayed by default when visiting the repo. exclude_doc_regexs+=(^README\.md$) # Exclude READMEs for test integration exclude_doc_regexs+=('^tests/cmd/.*/README\.md$') local exclude_pattern # Convert the list of files into an grep(1) alternation pattern. local IFS='|' # shellcheck disable=SC2034 exclude_pattern="${exclude_doc_regexs[*]}" unset IFS info "Checking document code blocks" # shellcheck disable=SC2034 local doc_to_script_cmd="${cidir}/kata-doc-to-script.sh" # Synchronisation point wait } static_check_eof() { local file="$1" local anchor="EOF" [[ -z "${file}" ]] && info "No files to check" && return # Skip the itself [[ "${file}" == "${script_name}" ]] && return # Skip the Vagrantfile [[ "${file}" == "Vagrantfile" ]] && return local invalid invalid=$(grep -o -E '<<-* *\w*' "${file}" |\ sed -e 's/^<<-*//g' |\ tr -d ' ' |\ sort -u |\ grep -v -E '^$' |\ grep -v -E "${anchor}" || true) [[ -z "${invalid}" ]] || die "Expected '${anchor}' here anchor, in ${file} found: ${invalid}" } # Tests to apply to all files. # # Currently just looks for TODO/FIXME comments that should be converted to # (or annotated with) an Issue URL. static_check_files() { local file local files if [[ "${force}" = "false" ]] then info "Skipping check_files: see https://github.com/kata-containers/tests/issues/469" return else info "Force override of check_files skip" fi info "Checking files" if [[ "${specific_branch}" = "true" ]] then info "Checking all files in ${branch} branch" files=$(git ls-files | grep -v -E "/(.git|vendor|grpc-rs|target)/" || true) else info "Checking local branch for changed files only" files=$(get_pr_changed_file_details || true) # Strip off status files=$(echo "${files}"|awk '{print $NF}') fi [[ -z "${files}" ]] && info "No files changed" && return local matches="" pushd "${repo_path}" for file in ${files} do local match # Look for files containing the specified comment tags but # which do not include a github URL. match=$(grep -H -E "\|\" "${file}" |\ grep -v "https://github.com/.*/issues/[0-9]" |\ cut -d: -f1 |\ sort -u || true) [[ -z "${match}" ]] && continue # Don't fail if this script contains the patterns # (as it is guaranteed to ;) echo "${file}" | grep -q "${script_name}$" && info "Ignoring special file ${file}" && continue # We really only care about comments in code. But to avoid # having to hard-code the list of file extensions to search, # invert the problem by simply ignoring document files and # considering all other file types. echo "${file}" | grep -q ".md$" && info "Ignoring comment tag in document ${file}" && continue matches+=" ${match}" done popd [[ -z "${matches}" ]] && return echo >&2 -n \ "ERROR: The following files contain TODO/FIXME's that need " echo >&2 -e "converting to issues:\n" for file in ${matches} do echo >&2 "${file}" done # spacer echo >&2 exit 1 } static_check_xml() { local all_xml local files pushd "${repo_path}" need_chronic all_xml=$(git ls-files "*.xml" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true) if [[ "${specific_branch}" = "true" ]] then info "Checking all XML files in ${branch} branch" files="${all_xml}" else info "Checking local branch for changed XML files only" local xml_status xml_status=$(get_pr_changed_file_details || true) xml_status=$(echo "${xml_status}" | grep "\.xml$" || true) files=$(echo "${xml_status}" | awk '{print $NF}') fi [[ -z "${files}" ]] && info "No XML files to check" && popd && return local file for file in ${files} do info "Checking XML file '${file}'" local contents # Most XML documents are specified as XML 1.0 since, with the # advent of XML 1.0 (Fifth Edition), XML 1.1 is "almost # redundant" due to XML 1.0 providing the majority of XML 1.1 # features. xmllint doesn't support XML 1.1 seemingly for this # reason, so the only check we can do is to (crudely) force # the document to be an XML 1.0 one since XML 1.1 documents # can mostly be represented as XML 1.0. # # This is only really required since Jenkins creates XML 1.1 # documents. contents=$(sed "s/xml version='1.1'/xml version='1.0'/g" "${file}") local ret { ${chronic} xmllint -format - <<< "${contents}"; ret=$?; } || true [[ "${ret}" -eq 0 ]] || die "failed to check XML file '${file}'" done popd } static_check_shell() { local all_scripts local scripts pushd "${repo_path}" need_chronic all_scripts=$(git ls-files "*.sh" "*.bash" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true) if [[ "${specific_branch}" = "true" ]] then info "Checking all scripts in ${branch} branch" scripts="${all_scripts}" else info "Checking local branch for changed scripts only" local scripts_status scripts_status=$(get_pr_changed_file_details || true) scripts_status=$(echo "${scripts_status}" | grep -E "\.(sh|bash)$" || true) scripts=$(echo "${scripts_status}" | awk '{print $NF}') fi [[ -z "${scripts}" ]] && info "No scripts to check" && popd && return 0 local script for script in ${scripts} do info "Checking script file '${script}'" local ret { ${chronic} bash -n "${script}"; ret=$?; } || true [[ "${ret}" -eq 0 ]] || die "check for script '${script}' failed" static_check_eof "${script}" done popd } static_check_json() { local all_json local json_files pushd "${repo_path}" need_chronic all_json=$(git ls-files "*.json" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true) if [[ "${specific_branch}" = "true" ]] then info "Checking all JSON in ${branch} branch" json_files="${all_json}" else info "Checking local branch for changed JSON only" local json_status json_status=$(get_pr_changed_file_details || true) json_status=$(echo "${json_status}" | grep "\.json$" || true) json_files=$(echo "${json_status}" | awk '{print $NF}') fi [[ -z "${json_files}" ]] && info "No JSON files to check" && popd && return 0 local json for json in ${json_files} do info "Checking JSON file '${json}'" local ret { ${chronic} jq -S . "${json}"; ret=$?; } || true [[ "${ret}" -eq 0 ]] || die "failed to check JSON file '${json}'" done popd } # The dockerfile checker relies on the hadolint tool. This function handle its # installation if it is not found on PATH. # Note that we need a specific version of the tool as it seems to not have # backward/forward compatibility between versions. has_hadolint_or_install() { # Global variable set by the caller. It might be overwritten here. linter_cmd=${linter_cmd:-"hadolint"} local linter_version linter_version=$(get_test_version "externals.hadolint.version") local linter_url linter_url=$(get_test_version "externals.hadolint.url") # shellcheck disable=SC2154 local linter_dest="${GOPATH}/bin/hadolint" local has_linter has_linter=$(command -v "${linter_cmd}" || true) if [[ -n "${has_linter}" ]]; then # Check if the expected linter version if "${linter_cmd}" --version | grep -v "${linter_version}" &>/dev/null; then warn "${linter_cmd} command found but not the required version ${linter_version}" has_linter="" fi fi if [[ -z "${has_linter}" ]]; then local download_url="${linter_url}/releases/download/v${linter_version}/hadolint-Linux-x86_64" info "Installing ${linter_cmd} ${linter_version} at ${linter_dest}" curl -sfL "${download_url}" -o "${linter_dest}" || \ die "Failed to download ${download_url}" chmod +x "${linter_dest}" # Overwrite in case it cannot be found in PATH. linter_cmd="${linter_dest}" fi } static_check_dockerfiles() { local all_files local files local ignore_files # Put here a list of files which should be ignored. local ignore_files=( ) pushd "${repo_path}" local linter_cmd="hadolint" all_files=$(git ls-files "*/Dockerfile*" | grep -Ev "/(vendor|grpc-rs|target)/" | sort || true) if [[ "${specific_branch}" = "true" ]]; then info "Checking all Dockerfiles in ${branch} branch" files="${all_files}" else info "Checking local branch for changed Dockerfiles only" local files_status files_status=$(get_pr_changed_file_details || true) files_status=$(echo "${files_status}" | grep -E "Dockerfile.*$" || true) files=$(echo "${files_status}" | awk '{print $NF}') fi [[ -z "${files}" ]] && info "No Dockerfiles to check" && popd && return 0 # As of this writing hadolint is only distributed for x86_64 if [[ "$(uname -m)" != "x86_64" ]]; then info "Skip checking as ${linter_cmd} is not available for $(uname -m)" popd return 0 fi has_hadolint_or_install linter_cmd+=" --no-color" # Let's not fail with INFO rules. linter_cmd+=" --failure-threshold warning" # Some rules we don't want checked, below we ignore them. # # "DL3008 warning: Pin versions in apt get install" linter_cmd+=" --ignore DL3008" # "DL3041 warning: Specify version with `dnf install -y -`" linter_cmd+=" --ignore DL3041" # "DL3033 warning: Specify version with `yum install -y -`" linter_cmd+=" --ignore DL3033" # "DL3018 warning: Pin versions in apk add. Instead of `apk add ` use `apk add =`" linter_cmd+=" --ignore DL3018" # "DL3003 warning: Use WORKDIR to switch to a directory" # See https://github.com/hadolint/hadolint/issues/70 linter_cmd+=" --ignore DL3003" # "DL3048 style: Invalid label key" linter_cmd+=" --ignore DL3048" # DL3037 warning: Specify version with `zypper install -y =`. linter_cmd+=" --ignore DL3037" # Temporary add to prevent failure for test migration # DL3040 warning: `dnf clean all` missing after dnf command. linter_cmd+=" --ignore DL3040" local file for file in ${files}; do if echo "${ignore_files[@]}" | grep -q "${file}" ; then info "Ignoring Dockerfile '${file}'" continue fi info "Checking Dockerfile '${file}'" local ret # The linter generates an Abstract Syntax Tree (AST) from the # dockerfile. Some of our dockerfiles are actually templates # with special syntax, thus the linter might fail to build # the AST. Here we handle Dockerfile templates. if [[ "${file}" =~ Dockerfile.*\.(in|template)$ ]]; then # In our templates, text with marker as @SOME_NAME@ is # replaceable. Usually it is used to replace in a # FROM command (e.g. `FROM @UBUNTU_REGISTRY@/ubuntu`) # but also to add an entire block of commands. Example # of later: # ``` # RUN apt-get install -y package1 # @INSTALL_MUSL@ # @INSTALL_RUST@ # ``` # It's known that the linter will fail to parse lines # started with `@`. Also it might give false-positives # on some cases. Here we remove all markers as a best # effort approach. If the template file is still # unparseable then it should be added in the # `$ignore_files` list. { sed -e 's/^@[A-Z_]*@//' -e 's/@\([a-zA-Z_]*\)@/\1/g' "${file}" | ${linter_cmd} -; ret=$?; }\ || true else # Non-template Dockerfile. { ${linter_cmd} "${file}"; ret=$?; } || true fi [[ "${ret}" -eq 0 ]] || die "failed to check Dockerfile '${file}'" done popd } static_check_rego() { local rego_files rego_files=$(git ls-files | grep -E '.*\.rego$') interpreters=("opa" "regorus") for interpreter in "${interpreters[@]}" do if ! command -v "${interpreter}" &>/dev/null; then die "Required rego interpreter '${interpreter}' not found in PATH" fi done found_unparsable=0 for file in ${rego_files} do for interpreter in "${interpreters[@]}" do if ! "${interpreter}" parse "${file}" > /dev/null; then info "Failed to parse Rego file '${file}' with ${interpreter}" found_unparsable=1 else info "Successfully parsed Rego file '${file}' with ${interpreter}" fi done done if [[ ${found_unparsable} -ne 0 ]]; then die "Unparsable rego files found" fi } # Run the specified function (after first checking it is compatible with the # users architectural preferences), or simply list the function name if list # mode is active. run_or_list_check_function() { local name="$1" func_is_valid "${name}" local arch_func local handler arch_func=$(func_is_arch_specific "${name}") handler="info" # If the user requested only a single function to run, we should die # if the function cannot be run due to the other options specified. # # Whereas if this script is running all functions, just display an # info message if a function cannot be run. [[ "${single_func_only}" = "true" ]] && handler="die" if [[ "${handle_funcs}" = "arch-agnostic" ]] && [[ "${arch_func}" = "yes" ]]; then if [[ "${list_only}" != "true" ]]; then "${handler}" "Not running '${func}' as requested no architecture-specific functions" fi return 0 fi if [[ "${handle_funcs}" = "arch-specific" ]] && [[ "${arch_func}" = "no" ]]; then if [[ "${list_only}" != "true" ]]; then "${handler}" "Not running architecture-agnostic function '${func}' as requested only architecture specific functions" fi return 0 fi if [[ "${list_only}" = "true" ]]; then echo "${func}" return 0 fi info "Running '${func}' function" eval "${func}" } setup() { # shellcheck source=/dev/null source /etc/os-release || source /usr/lib/os-release trap remove_tmp_files EXIT } # Display a message showing some system details. announce() { local arch arch=$(uname -m) local file='/proc/cpuinfo' local detail detail=$(grep -m 1 -E '\|\ * *:' "${file}" \ 2>/dev/null |\ cut -d: -f2- |\ tr -d ' ' || true) local arch="${arch}" [[ -n "${detail}" ]] && arch+=" ('${detail}')" local kernel kernel=$(uname -r) local distro_name local distro_version distro_name="${NAME:-}" distro_version="${VERSION:-}" local -a lines local IFS=$'\n' mapfile -t lines <<-EOF Running static checks: script: ${script_name} architecture: ${arch} kernel: ${kernel} distro: name: ${distro_name} version: ${distro_version} EOF local line for line in "${lines[@]}" do info "${line}" done } main() { setup local long_option_names="${!long_options[*]}" local args args=$(getopt \ -n "${script_name}" \ -a \ --options="h" \ --longoptions="${long_option_names}" \ -- "$@") # shellcheck disable=SC2181 [[ $? -eq 0 ]] || { usage >&2; exit 1; } eval set -- "${args}" local func= repo_path="" while [[ $# -gt 1 ]] do case "$1" in --all) specific_branch="true" ;; --branch) branch="$2"; shift ;; --commits) func=static_check_commits ;; --docs) func=static_check_docs ;; --dockerfiles) func=static_check_dockerfiles ;; --files) func=static_check_files ;; --force) force="true" ;; --golang) func=static_check_go_arch_specific ;; -h|--help) usage; exit 0 ;; --json) func=static_check_json ;; --labels) func=static_check_labels;; --licenses) func=static_check_license_headers ;; --list) list_only="true" ;; --no-arch) handle_funcs="arch-agnostic" ;; --only-arch) handle_funcs="arch-specific" ;; --rego) func=static_check_rego ;; --repo) repo="$2"; shift ;; --repo-path) repo_path="$2"; shift ;; --scripts) func=static_check_shell ;; --versions) func=static_check_versions ;; --xml) func=static_check_xml ;; --) shift; break ;; esac shift done # Consume getopt cruft [[ "$1" = "--" ]] && shift [[ "$1" = "help" ]] && usage && exit 0 # Set if not already set by options [[ -z "${repo}" ]] && repo="$1" [[ "${specific_branch}" = "false" ]] && specific_branch="$2" if [[ -z "${repo}" ]] then # No repo param provided so assume it's the current # one to avoid developers having to specify one now # (backwards compatability). repo=$(git config --get remote.origin.url |\ sed 's!https://!!g' || true) info "Auto-detected repo as ${repo}" fi test_path="${test_path:-"${repo}/tests"}" test_dir="${GOPATH}/src/${test_path}" if [[ -z "${repo_path}" ]]; then repo_path="${GOPATH}/src/${repo}" else test_dir="${repo_path}/${test_path}" fi announce local all_check_funcs all_check_funcs=$(typeset -F|awk '{print $3}'|grep "${check_func_regex}"|sort || true) # Run user-specified check and quit if [[ -n "${func}" ]]; then single_func_only="true" run_or_list_check_function "${func}" exit 0 fi for func in ${all_check_funcs} do run_or_list_check_function "${func}" done } main "$@"