Files
kata-containers/tests/static-checks.sh
stevenhorsman 9398ab3433 ci: remove docs URL alive check workflow
The docs URL alive check workflow has been disabled for months
and never passed since we moved to GHA., so removes the workflow
and all associated code.

Note: Although the static-checks.sh --doc code was wider scope than
URL check, it wasn't being called anywhere else, so it was removed too.

Signed-off-by: stevenhorsman <steven@uk.ibm.com>
Assisted-by: IBM Bob
2026-05-13 09:12:00 +01:00

1304 lines
33 KiB
Bash
Executable File

#!/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 <<EOF
Usage: ${script_name} help
${script_name} [options] [repo-name] [true]
Options:
EOF
local option
local description
local long_option_names="${!long_options[*]}"
# Sort space-separated list by converting to newline separated list
# and back again.
long_option_names=$(echo "${long_option_names}"|tr ' ' '\n'|sort|tr '\n' ' ')
# Display long options
for option in ${long_option_names}
do
description=${long_options[${option}]}
# Remove any trailing colon which is for getopt(1) alone.
option="${option%%:}"
printf " --%-10.10s # %s\n" "${option}" "${description}"
done
cat <<EOF
Parameters:
help : Show usage.
repo-name : GitHub URL of repo to check in form "github.com/user/repo"
(equivalent to "--repo \$URL").
true : Specify as "true" if testing a specific branch, else assume a
PR branch (equivalent to "--all").
Notes:
- If no options are specified, all non-skipped tests will be run.
- Some required tools may be installed in \$GOPATH/bin, so you should ensure
that it is in your \$PATH.
Examples:
- Run all tests on a specific branch (stable or main) of kata-containers repo:
$ ${script_name} github.com/kata-containers/kata-containers true
- Auto-detect repository and run golang tests for current repository:
$ ${script_name} --golang
- Run all tests on the kata-containers repository, forcing the tests to
consider all files, not just those changed by a PR branch:
$ ${script_name} github.com/kata-containers/kata-containers --all
EOF
}
# Calls die() if the specified function is not valid.
func_is_valid() {
local name="$1"
type -t "${name}" &>/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 <value>').
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[*]}'"
}
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 "\<FIXME\>|\<TODO\>" "${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 <package>-<version>`"
linter_cmd+=" --ignore DL3041"
# "DL3033 warning: Specify version with `yum install -y <package>-<version>`"
linter_cmd+=" --ignore DL3033"
# "DL3018 warning: Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`"
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 <package>=<version>`.
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 '\<vendor_id\>|\<cpu\> * *:' "${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 ;;
--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 "$@"